Arpit16112's picture
Updated train.py & app.py
4cb5173
Raw
History Blame Contribute Delete
36.4 kB
# --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
# Project :- StyleXfer – "Adaptive Neural Style Transfer"
# StyleXfer is an AI‑powered project built on Adaptive Instance Normalization (AdaIN) that seamlessly transfers artistic styles onto images.
# By intelligently aligning feature statistics between content and style, it produces visually striking transformations while preserving the essence of the original image.
# Designed to be lightweight, efficient, and creative, StyleXfer showcases the fusion of deep learning and digital artistry.
#----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
## Steps to run this file to train model on kaggle :-
# Here we will use Kaggle for training this model because Kaggle provide free 16GB GPU, which helps in faster training
# So for that we firstly need to create the zip file of this 'StyleXfer_NST_code' folder as all of our model related code is inside this folder only
# And then we will go to :- https://www.kaggle.com/code
# Then we will create a new notebook & then we will upload our ZIP folder as the new Dataset for that notebook because in kaggle, even folder can be uploaded as dataset only
# And then we will copy the path of our main python file i.e train.py & then write this code in notebook cell :-
# !python </kaggle/input/datasets/arpitpal07/stylexfer-model-training/StyleXfer_NST_code/train.py> --batch_size 4 --epochs 160 --experiment='final_experiment' --save_interval 15 --content_dir='/kaggle/input/datasets/arpitpal07/stylexfer-model-training/StyleXfer_NST_code/content_dataset' --style_dir='/kaggle/input/datasets/arpitpal07/stylexfer-model-training/StyleXfer_NST_code/style_dataset' --vgg='/kaggle/input/datasets/arpitpal07/stylexfer-model-training/StyleXfer_NST_code/vgg_normalised.pth'
# ANd we running this, we also need to go to sessions options -> accelerator -> select GPU P100 -> it will turn on the free GPU
# Then start the session by clicking on the start session button
# And run that cell -> It will start the training of model
#----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
## Steps for proper training :-
# we will firstly run this file with these arguments:-
# python train.py --batch_size 16 --epochs 160 --experiment='final_training'
# And then we will change the batch_size to 8 & final_size to 512 for better images & style_weight to 10 & resume = True, so that we can start the training where we left lastly i.e from 160th epoch
# So then we will run this file using these parameters :-
# python train.py --batch_size 8 --epochs 200 --experiment='final_training' --final_size 512 --style_weight 10 --resume --decoder_path='experiment/final_training/decoder_160.pth' --optimizer_path='experiment/final_training/optimizer_160.pth'
#----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
# In this file, we will actually write the logic for training our model for NST.
# This file is essentially our training script for the AdaIN Neural Style Transfer project.
# This file is our training driver script. It :-
# Loads datasets and models.
# Applies AdaIN to fuse content + style features.
# Trains the decoder to reconstruct stylized images.
# Logs progress and saves checkpoints + sample outputs.
import sys
from pathlib import Path
# Add parent directory to Python path so imports work from any location
sys.path.insert(0, str(Path(__file__).parent.parent))
import argparse # importing Python’s argparse module, which is part of the standard library.
# It lets us define the command‑line arguments for our script (like --epochs, --batch_size, --content_dir).
# When we run our script from the terminal, we can pass in values without editing the code.
# e.g python train.py --epochs 10 --batch_size 8 --content_dir ./data/content --style_dir ./data/style
import torch
# Here we are importing the DataLoader class from PyTorch’s torch.utils.data module
from torch.utils.data import DataLoader
# DataLoader is PyTorch’s utility for loading datasets efficiently. It wraps around a Dataset (like your ImageFolderDataset) and provides:
# Batching → splits data into mini‑batches (e.g., 16 images at a time).
# Shuffling → randomizes the order of samples each epoch.
# Parallel loading → can use multiple worker threads to load data faster.
# Iteration → makes datasets iterable in training loops.
# torch.optim is PyTorch’s package that contains different optimization algorithms (like SGD, Adam, RMSprop).
# These optimizers are used to update the trainable parameters of your model (in this case, the decoder) during training.
import torch.optim as optim # here we are importing optimizer actually
from pathlib import Path
# Here we are importing everything i.e classes or functions etc from the utils.py file present inside the utils folder
from StyleXfer_NST_code.utils.utils import *
# Here we are importing everything i.e classes or functions etc from the models.py file present inside the utils folder
from StyleXfer_NST_code.utils.models import *
# tqdm is a Python library that gives you progress bars for loops
from tqdm import tqdm
# save_image :- A function that saves a PyTorch tensor as an image file (e.g., .png, .jpg).
# The tensor should have shape [C, H, W] (channels, height, width) or [B, C, H, W] (batch of images).
# It automatically converts the tensor values into pixel values and writes them to disk.
from torchvision.utils import save_image
# function that sets up command‑line argument handling for our script.
# When called, it will return an object (args) containing the values the user passed in when running the script.
def parse_arguments():
# Creates an ArgumentParser object from Python’s argparse module.
parser = argparse.ArgumentParser()
# It will hold all the arguments we define with parser.add_argument(...).
# --content_dir :- This is the name of the argument you can pass when running your script.
# e.g python train.py --content_dir /user/images/content
# If you don’t provide it, the script will use this default value.
# type=str :- Specifies that the argument must be a string (in this case, a file path). If you try to pass something invalid (like a number when a string is expected), argparse will throw an error.
# help='Location of content dataset' :- This is the description shown when you run: python train.py --help
# It tells the user what the argument is for.
parser.add_argument('--content_dir', type=str, default='./content_data_examples', help='Location of content dataset')
parser.add_argument('--style_dir', type=str, default='./style_data_examples', help='Location of style dataset')
# Here we will use this pre-trained VGG model for extracting the feature maps from images.
# Here we are defining a command‑line argument for your script that tells it where to find the pretrained VGG model file
parser.add_argument('--vgg', type=str, default='vgg_normalised.pth', help='Location of pre-trained VGG')
# A .pth file in PyTorch is simply a checkpoint file that stores model parameters (weights, biases, optimizer states, etc.) in a serialized format.
# Inside .pth, it contains tensors saved with torch.save().
# Depending on how you save it, a .pth file can hold:
# Model weights (state_dict of a neural network).
# Optimizer state (momentum, learning rate, etc.).
# Entire checkpoint (model + optimizer + epoch info).
# So, If we don’t explicitly pass --experiment, the script assumes the experiment name is "experiment1".
parser.add_argument('--experiment', type=str, default='experiment1', help='Name of experiment')
# The experiment name is used to create a save directory: save_dir = Path('experiment') / args.experiment
# Here we are defining a command‑line argument that controls the output image size.
# If you don’t explicitly pass --final_size, the script assumes the final image size is 512 pixels (usually width × height).
parser.add_argument('--final_size', type=int, default=256, help='Size of final image')
parser.add_argument('--content_size', type=int, default=512, help='Size of content image')
parser.add_argument('--style_size', type=int, default=512, help='Size of style image')
# Here we are defining a command‑line flag that controls whether images should be cropped during preprocessing
# Since it uses action='store_true', we don’t need to provide a value — just including --crop sets it to True.
# default=True :- Sets the default value to True if you don’t provide the flag. So even if you don’t type --crop, cropping will be enabled by default.
parser.add_argument('--crop', action='store_true', default=True, help='Crop image')
parser.add_argument('--batch_size', type=int, default=4, help='Batch size')
# Here this default value is a common choice for training the decoder in AdaIN style transfer.
parser.add_argument('--lr', type=float, default=1e-4, help='Learning rate')
parser.add_argument('--lr_decay', type=float, default=5e-5, help='Learning rate decay')
parser.add_argument('--epochs', type=int, default=1, help='Number of epochs')
parser.add_argument('--content_weight', type=float, default=1.0, help='Content weight')
parser.add_argument('--style_weight', type=float, default=5, help='Style weight')
# It is a command‑line argument for logging frequency in our training script
# If you don’t specify --log_interval when running the script, it defaults to 1. Meaning: log after every batch by default.
# log_interval determines how often training information is printed/logged (like losses).
parser.add_argument('--log_interval', type=int, default=1, help='Log interval')
# Here we are adding a command‑line argument for how often to save model checkpoints during training
# If you don’t specify --save_interval, the script defaults to saving every 2 epochs. Meaning: after every 2 epochs, the model checkpoint will be saved automatically.
# save_interval determines how frequently the model is saved during training.
parser.add_argument('--save_interval', type=int, default=2, help='Save interval')
parser.add_argument('--resume', action='store_true', default=False, help='Resume training')
parser.add_argument('--decoder_path', type=str, default=None, help='Path to decoder checkpoint')
parser.add_argument('--optimizer_path', type=str, default=None, help='Path to optimizer checkpoint')
# It tells the argparse parser to read the actual command‑line arguments we passed when running the script i.e this train.py file.
# e.g python train.py --epochs 10 --batch_size 8 → parse_args() will grab those values and store them in an object.
return parser.parse_args()
# Here in this main() fn, we will write all of our training code actually
def main():
args = parse_arguments()
# print(args)
# torch.device(...) :- Creates a device object that tells PyTorch where tensors and models should live.
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# Later in our code, we can move models and data to this device:
# model.to(device)
# tensor.to(device)
# Here we are creating the folder path where our training outputs (checkpoints, logs, images) will be saved.
save_dir = Path('experiment') / args.experiment
# Path('experiment') :- It represents a filesystem path object (instead of just a plain string).
# Here, it points to a base folder called "experiment".
# / args.experiment :- The / operator is overloaded in pathlib to mean path joining.
# So, os.path.join("experiment", args.experiment) ,you can simply write: Path("experiment") / args.experiment
# exist_ok=True :- Means: “Don’t throw an error if the folder already exists.”
save_dir.mkdir(exist_ok=True, parents=True) # is creating the directory on disk
# parents=True :- means “Create all parent directories if they don’t exist.”
# Example: If you ask for experiment/experiment1, but experiment/ doesn’t exist yet, it will create both experiment/ and experiment/experiment1.
# Without this, it would fail unless the parent folder already existed.
# Saving arguments values
# Saving all the command‑line arguments we passed into a text file, so we have a record of how the experiment was run.
# The with statement ensures the file is properly closed after writing, even if errors occur.
with open(save_dir/'args.txt', 'w') as args_file:
# args is the Namespace object returned by parser.parse_args().
# vars(args) converts it into a dictionary & .items() gives you key–value pairs (like ('epochs', 10)).
for key, value in vars(args).items():
args_file.write(f'{key}: {value}\n')
content_transform = get_transform(args.content_size, args.crop, args.final_size)
style_transform = get_transform(args.style_size, args.crop, args.final_size)
# Now to read the data using the custom dataset which we created inside utils.py file, we will firstly create the object of ImageFOlderDataset class
# And then we will pass them to DataLoader, so that it can creates the batches from that
content_dataset = ImageFolderDataset(args.content_dir, content_transform)
style_dataset = ImageFolderDataset(args.style_dir, style_transform)
# Now we will create the dataloaders from these datasets, which will actually create the batches of images & then return those batches from these datasets
content_dataloader = DataLoader(content_dataset, batch_size=args.batch_size, shuffle = True, pin_memory=True, drop_last=True)
# batch_size=args.batch_size :- Groups samples into batches of size args.batch_size. Example: if args.batch_size=16, each iteration returns a tensor of shape [16, 3, H, W].
# shuffle=True :- Randomizes the order of samples each epoch. Prevents the model from memorizing the dataset order and improves generalization.
# SO it means that after every epoch, our dataset gets shuffled which prevents the model from memorizing the dataset order after every epoch
# pin_memory=True :- Allocates batches in page‑locked (pinned) memory. Speeds up data transfer from CPU → GPU. Useful when training on CUDA devices.
# drop_last=True :- Drops the last batch if it’s smaller than batch_size.
# Ensures all batches are the same size (important for some models that expect fixed batch dimensions).
# Example: if you have 103 images and batch size 16 → Normally: last batch would have 7 images. With drop_last=True: last batch is discarded, so you only get 96 images (6 full batches).
style_dataloader = DataLoader(style_dataset, batch_size=args.batch_size, shuffle = True, pin_memory=True, drop_last=True)
# We do not need shuffling after every epoch in case of test dataloaders.
# Training vs Testing DataLoader :-
## Training DataLoader (shuffle=True) :-
# - Shuffling is important during training because it prevents the model from memorizing the order of samples.
# - It improves generalization by ensuring batches are different each epoch.
## Testing/Validation DataLoader (shuffle=False) :-
# Here, we want deterministic evaluation.
# The model should see the test samples in a fixed order every time.
# This ensures reproducibility — the same inputs always produce the same outputs.
# Why No Shuffling in Testing :-
# If you shuffle test data, results may vary slightly between runs (different batch composition).
# That makes it harder to compare metrics (accuracy, loss, etc.) across experiments.
# Keeping the order fixed ensures consistent evaluation.
print('Number of batches in content dataset: ', len(content_dataloader))
print('Number of batches in style dataset: ', len(style_dataloader))
# for batch in style_dataloader:
# print(batch.shape) # here we are usinf .shape because batch are also dataset only but of smaller size as comapred to original dataset
encoder = VGGEncoder(args.vgg).to(device)
# The decoder is the trainable network that reconstructs stylized images from encoded features.
# .to(device) moves the model to the specified hardware device. This ensures all computations (forward pass, training) happen on the chosen device
decoder = Decoder().to(device)
# Now we will use optimizer to update the weights
# optim.Adam :- Chooses the Adam optimizer, one of the most popular algorithms in deep learning.
# Adam combines the benefits of momentum (like SGD with momentum) and adaptive learning rates (like RMSprop). It’s well‑suited for training deep networks such as your decoder.
# decoder.parameters() :- Passes all the trainable parameters (weights and biases) of the decoder into the optimizer.
# Since the encoder is frozen (requires_grad=False), only the decoder’s parameters will be updated.
# lr=args.lr :- Sets the learning rate (step size for updates).
# args.lr means the learning rate is taken from command‑line arguments or a config file (so you can easily change it without editing code).
# Typical values for AdaIN style transfer are around 1e-4.
optimizer = optim.Adam(decoder.parameters(), lr=args.lr)
# Learning rate decay is a technique to reduce the learning rate over time.
# Early in training → larger steps (fast learning).
# Later in training → smaller steps (fine‑tuning).
# Prevents overshooting and helps stabilize convergence.
# In AdaIN style transfer, this ensures the decoder learns steadily without destabilizing the pretrained encoder features.
# Here we are setting up a learning rate scheduler in PyTorch — a mechanism to automatically adjust the learning rate as training progresses.
# optim.lr_scheduler.LambdaLR :- A PyTorch scheduler that lets you define a custom function (lr_lambda) to control how the learning rate changes over epochs.
# It wraps around your optimizer and modifies its learning rate at each step.
# lr_lambda = lambda epoch: 1.0 / (1.0 + args.lr_decay * epoch) :- Defines the decay function for the learning rate.
# At epoch 0 → factor = 1.0 / (1.0 + 0) = 1.0 (no decay).
# At epoch 1 → factor = 1.0 / (1.0 + args.lr_decay * 1).
# At epoch N → factor = 1.0 / (1.0 + args.lr_decay * N).
# This means the learning rate shrinks gradually as epochs increase.
scheduler = optim.lr_scheduler.LambdaLR(
optimizer,
lr_lambda = lambda epoch: 1.0 / (1.0 + args.lr_decay * epoch)
)
# here we are handling the resume training functionality — it reloads saved model and optimizer states so you can continue training from where you left off.
if args.resume: # Checks if the --resume flag was passed when running the script.
# If true, it will reload previously saved training progress.
# decoder.load_state_dict(torch.load(args.decoder_path)) :- Loads the decoder’s weights from a checkpoint file (args.decoder_path).
# torch.load(...) reads the saved state dictionary (layer weights and biases).
# load_state_dict(...) restores those weights into the decoder model.
# This means the decoder doesn’t start from scratch — it continues from where it was last saved.
decoder.load_state_dict(torch.load(args.decoder_path))
# Loads the optimizer’s state (Adam’s internal parameters like momentum buffers, learning rate schedule, etc.).
# Ensures the optimizer resumes with the same learning dynamics as before.
# Without this, even if the decoder weights are restored, the optimizer would “forget” its progress and restart fresh.
optimizer.load_state_dict(torch.load(args.optimizer_path))
# Why It’s Important :-
# Training style transfer models can take hours or days.
# You don’t want to lose progress if training is interrupted.
# By saving both:
# Decoder weights → the model’s learned knowledge.
# Optimizer state → the training momentum and learning rate adjustments.
# You can resume training seamlessly, as if nothing was interrupted.
print('Training...')
# Now for training loop, we need to firstly calculate the loss
# And for that we will use mean squared loss actually
# In style transfer:
# Often used to compare feature maps (content loss) or Gram matrices (style loss).
# It penalizes large differences more strongly, encouraging the decoder to reconstruct images close to the target.
mse_loss = torch.nn.MSELoss()
# Sets the encoder (VGG) to evaluation mode.
# In PyTorch, models can be in:
# Training mode (model.train()) → layers like dropout and batch normalization behave differently (they update statistics).
# Evaluation mode (model.eval()) → those layers stop updating and use fixed behavior.
# Since the encoder is pretrained and frozen, we don’t want it to change during training.
# eval() ensures it acts purely as a fixed feature extractor.
encoder.eval()
running_loss = None # total loss
running_closs = None # content loss
running_sloss = None # style loss
# Now we will setup the training loop with a progress bar
for epoch in range(args.epochs):
progress_bar = tqdm(
zip(content_dataloader, style_dataloader),
total=min(len(content_dataloader), len(style_dataloader))
)
# zip(content_dataloader, style_dataloader) :- Combines the two dataloaders (content images and style images) into pairs.
# Each iteration gives you one batch of content images and one batch of style images.
# e.g for content_batch, style_batch in zip(content_dataloader, style_dataloader):
# tqdm(...) :- Wraps the loop with a progress bar.
# Shows how many batches have been processed, speed, and estimated time remaining.
# total=min(len(content_dataloader), len(style_dataloader)) :- Ensures the progress bar length matches the smaller of the two datasets.
# Prevents errors if content and style datasets have different sizes.
# Training stops when the shorter dataloader runs out of batches
running_loss = 0
running_closs = 0
running_sloss = 0
# Iterates over the progress bar created earlier (tqdm(zip(content_dataloader, style_dataloader))).
# Each iteration gives you:
# content_batch: a batch of content images.
# style_batch: a batch of style images.
# These are paired together so the model can apply style transfer.
for content_batch, style_batch in progress_bar:
content_batch = content_batch.to(device)
style_batch = style_batch.to(device)
# Now firstly every image needs to pass through the encoder to get its feature map
# encoder(content_batch) :- Passes the batch of content images through the pretrained VGG encoder. The encoder outputs feature maps (multi‑level representations of the image).
# These features capture the structure, shapes, and semantic content of the image. Stored in c_feats.
c_feats = encoder(content_batch)
# encoder(style_batch) :- Passes the batch of style images through the same encoder.
# Outputs feature maps that capture textures, colors, and patterns of the style image. Stored in s_feats.
s_feats = encoder(style_batch)
# So here these 'c_feats' & 's_feats' will be actually tuple of feature maps
# print(len(c_feats))
# print(len(s_feats))
# print(type(c_feats))
# print(c_feats)
# print(c_feats[0].shape)
# Here this 'adaptive_instance_normalization' will actually apply the AdaIN layer on these feature maps i.e we are passing the outputs of encoder to this AdaIN layer as per NST using AdaIN Architecture algorithm
# c_feats and s_feats are lists (or tuples) of feature maps from the encoder at different layers.
# [-1] selects the deepest feature map (usually from relu4_1 in VGG).
# These are the most semantically rich features:
# c_feats[-1] → content structure. So c_feats[-1] selects the deepest feature map (the one from the last layer the encoder outputs).
# s_feats[-1] → style texture/color statistics.
t = adaptive_instance_normalization(c_feats[-1], s_feats[-1])
# adaptive_instance_normalization(c_feats[-1], s_feats[-1]) :- AdaIN aligns the mean and variance of the content features with those of the style features, so that we can say that style gets transfer to content image
# AdaIN(𝑐,𝑠) = (𝜎(𝑠) ⋅ ((𝑐 − 𝜇(𝑐))/ 𝜎(𝑐))) + 𝜇(𝑠)
# Intuition:
# Normalize content features → remove their original style.
# Re‑scale and re‑center them using style statistics → inject style appearance.
# Result: t is a tensor of blended features (content structure + style appearance).
# The decoder takes the blended features t and reconstructs them back into an RGB image.
# This output g is the stylized image:
# Preserves the layout/structure of the content image.
# Painted with the textures/colors of the style image.
g = decoder(t) # it represent the output i.e generated image
# Now as decoder is trainable, so we need to update its weights & for that we need to firstly find the loss
# And for loss, we need feature map of this g, so we will pass it through encoder
g_feats = encoder(g)
# g_feats[-1] :- These are the features of the generated image (g) extracted by the encoder.
# [-1] means the deepest feature map (high‑level representation of the generated image).
# t :- This is the target blended feature map produced by AdaIN. It represents the content structure aligned with the style statistics.
loss_c = mse_loss(g_feats[-1], t) * args.content_weight
# mse_loss(g_feats[-1], t) :- Computes the Mean Squared Error (MSE) between:
# The generated image’s features (g_feats[-1]). And the The target AdaIN features (t).
# This measures how close the generated image is to the desired blended representation.
# Here t is actually shows the feature maps og content imahe which is slightly gets transformed
# Although, instead of t, we can use c_feats, but authors found that using t gives better results, that's why we are using it here.
# * args.content_weight :- Scales the loss by a user‑defined weight (--content_weight).
# Allows you to control the balance between content preservation and style transfer:
# Higher content weight → generated image sticks more closely to the original content structure.
# Lower content weight → style dominates more strongly.
# Now we need to calculate the style loss & it will calculated between style image & generated image
loss_s = 0
for g_f, s_f in zip(g_feats, s_feats):
# calc_mean_std(g_f) :- Computes the channel‑wise mean and standard deviation of the feature map g_f.mThese statistics capture the style information (color distribution, texture patterns). Returns (g_mean, g_std).
g_mean, g_std = calc_mean_std(g_f)
s_mean, s_std = calc_mean_std(s_f)
# mse_loss(g_mean, s_mean) + mse_loss(g_std, s_std) :- Compares the generated image’s statistics with the style image’s statistics.
# If they match, the generated image has successfully adopted the style. MSE ensures the generated mean and variance are close to the style’s mean and variance.
loss_s += mse_loss(g_mean, s_mean) + mse_loss(g_std, s_std)
# * args.style_weight :- Multiplies the style loss by a weight specified in the command‑line arguments (--style_weight).
# This weight controls how strongly the style influences the final output.
loss_s = loss_s * args.style_weight
# If args.style_weight is large → the stylized image will emphasize textures, colors, and patterns of the style image more strongly.
# If args.style_weight is small → the stylized image will preserve more of the content structure and be less stylized.
loss = loss_c + loss_s # this is the total loss
# Now we will do back propagation to train this decoder i.e to update its weights using loss functions
# optimizer.zero_grad() :- Clears (resets) all previously stored gradients in the model parameters.
# PyTorch accumulates gradients by default, so if you don’t reset them, they’ll keep adding up across iterations. This ensures each training step starts fresh.
optimizer.zero_grad()
loss.backward() # Performs backpropagation: computes the gradient of the loss with respect to all model parameters.
optimizer.step() # Updates the model’s parameters using the optimizer (e.g., Adam, SGD).
# Here we are updating the progress bar’s description so we can see the current losses while training.
# progress_bar.set_description(...) :- tqdm progress bars let you attach a custom description string that appears alongside the bar. This is useful for showing dynamic info (like losses) during training.
progress_bar.set_description(f'Loss:{loss.item():4f}, Content Loss: {loss_c.item():4f}, Style Loss: {loss_s.item():4f}')
# loss.item() :- Converts the PyTorch tensor loss into a regular Python float.
# Represents the total loss (content + style, weighted) for the current batch.
running_loss += loss.item()
running_closs += loss_c.item()
running_sloss += loss_s.item()
# Now we will update the learning rate scheduler and then computing the average losses per epoch
scheduler.step()
# This gives the average total loss per batch for the epoch.
running_loss /= len(content_dataloader)
running_closs /= len(content_dataloader)
running_sloss /= len(content_dataloader)
# Checks whether the current epoch number (plus 1, since epochs are zero‑indexed) is divisible by the logging interval.
# Example: If log_interval=2, it will log at epochs 2, 4, 6, etc. If log_interval=1, it logs every epoch.
# tqdm.write(...) :- Prints a message above the progress bar without breaking its formatting.
# Useful for clean logging when using tqdm.
if (epoch+1) % args.log_interval == 0:
tqdm.write(f'Iter {epoch+1}: Loss:{running_loss:4f}, Content Loss: {running_closs:4f}, Style Loss: {running_sloss:4f}')
# torch.save(decoder.state_dict(), ...) :- Saves the decoder’s parameters (weights) to a file named decoder_<epoch>.pth. This lets you resume training or reuse the trained decoder later.
# torch.save(optimizer.state_dict(), ...) :- Saves the optimizer state (learning rate, momentum, etc.) to a file named optimizer_<epoch>.pth. Important for resuming training exactly where you left off.
if (epoch+1) % args.save_interval == 0:
torch.save(decoder.state_dict(), save_dir / f'decoder_{epoch+1}.pth')
torch.save(optimizer.state_dict(), save_dir / f'optimizer_{epoch+1}.pth')
# Temporarily disables gradient tracking (since we’re just generating an output, not training). Makes the operation faster and saves memory.
# here we can use these content_batch, g etc because In Python, variables defined inside a for loop are not limited to the loop’s scope.
# here these actually hold the last values assigned during the final iteration of the loop.
with torch.no_grad():
# Concatenates three sets of images along the batch dimension:
# Original content images. And Original style images. And Generated stylized images (g).
# This way, you can visually compare them side by side.
# torch.cat concatenates tensors along a specified dimension. Here, dim=0 means concatenation along the batch dimension (the first axis).
# So instead of stacking images side‑by‑side in width or height, you’re stacking them as if they were part of one bigger batch.
output = torch.cat([content_batch, style_batch, g], dim=0)
# [content_batch, style_batch, g] :-
# Three tensors are being concatenated:
# content_batch → the original content images.
# style_batch → the original style images.
# g → the generated stylized images.
# Each of these has shape like [N, 3, H, W] (batch size, channels, height, width).
# After concatenation, output is a single tensor containing all three sets of images.
# Example: if each batch has 16 images, the result will have: torch.Size([48, 3, H, W]). That’s 16 content + 16 style + 16 generated images.
# Makes it easy to save them together in one grid image using save_image.
# save_image(output, ...) :- save_image is a PyTorch utility (torchvision.utils.save_image) that saves a batch of image tensors as a single image file. It arranges them into a grid for easy visualization
# Here this output contains Content images, Style images & Generated stylized images. All stacked together as one big batch.
save_image(output, save_dir / f'output_{epoch+1}.png', nrow=args.batch_size)
# nrow=args.batch_size :- Controls how many images are placed per row in the grid.
# If your batch size is 16, each row will contain 16 images. This makes the saved image neatly organized.
if __name__ == '__main__':
main()
# Every Python file has a special built‑in variable called __name__.
# If the file is being run directly (e.g., python train.py), then __name__ is set to "__main__".
# If the file is being imported as a module into another script, then __name__ is set to the module’s name (e.g., "train").
# Why use if __name__ == '__main__':
# It ensures that the code inside runs only when the file is executed directly, not when imported.
# In our case, it calls main(), which starts the whole training process (argument parsing, dataset loading, model setup, training loop, saving checkpoints).
# If someone imports this file (e.g., to reuse VGGEncoder, Decoder, or utility functions), the training won’t auto‑start — only the functions/classes will be available.