Pretrained image models can carry useful visual features into a smaller classification project without relearning every convolutional filter. A fixed-feature-extractor pass keeps those learned features intact while a new classifier head adapts ResNet18 to the classes in a project dataset.
The ResNet18_Weights.DEFAULT enum identifies the pretrained weights and supplies their matching resize, crop, and normalization transform. The frozen feature extractor stays in evaluation mode so BatchNorm running statistics remain unchanged, while the replacement fc layer stays in training mode.
Project class labels come from the training directory names that ImageFolder discovers, so the saved class order must travel with the model state. The completed run reports the trainable parameter names, epoch metrics, output shape, frozen-backbone check, classifier-head change, and checkpoint path.
Related: How to install PyTorch with pip
data/
└── document-images/
└── train/
├── invoice/
│ ├── invoice-01.png
│ ├── invoice-02.png
│ ├── invoice-03.png
│ └── invoice-04.png
├── receipt/
│ ├── receipt-01.png
│ ├── receipt-02.png
│ ├── receipt-03.png
│ └── receipt-04.png
└── statement/
├── statement-01.png
├── statement-02.png
├── statement-03.png
└── statement-04.png
Each immediate subdirectory becomes one class name, and every class needs enough representative images for training.
Related: How to create a custom Dataset in PyTorch
import torch from torch import nn from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder from torchvision.models import ResNet18_Weights, resnet18 torch.manual_seed(29) weights = ResNet18_Weights.DEFAULT train_data = ImageFolder( "data/document-images/train", transform=weights.transforms(), ) train_loader = DataLoader(train_data, batch_size=4, shuffle=True) class_names = train_data.classes
The weight-bound transform keeps image size and normalization aligned with the pretrained ResNet18 weights.
model = resnet18(weights=weights) for parameter in model.parameters(): parameter.requires_grad = False model.fc = nn.Linear(model.fc.in_features, len(class_names)) optimizer = torch.optim.SGD(model.fc.parameters(), lr=0.01, momentum=0.9) loss_fn = nn.CrossEntropyLoss()
New Linear parameters remain trainable by default, and passing only model.fc.parameters() keeps the optimizer scoped to the replacement head.
Related: How to freeze model layers in PyTorch
backbone_before = model.conv1.weight.detach().clone() head_before = model.fc.weight.detach().clone() model.eval() model.fc.train() loss_total = 0.0 correct = 0 sample_count = 0
model.eval() prevents frozen BatchNorm buffers from changing, and model.fc.train() keeps the replacement classifier in training mode.
for images, targets in train_loader: optimizer.zero_grad(set_to_none=True) logits = model(images) loss = loss_fn(logits, targets) loss.backward() optimizer.step() loss_total += loss.item() * images.size(0) correct += (logits.argmax(dim=1) == targets).sum().item() sample_count += images.size(0)
The optimizer updates the replacement classifier after each batch while the frozen backbone supplies features.
Related: How to run a training loop in PyTorch
trainable_parameters = [ name for name, parameter in model.named_parameters() if parameter.requires_grad ] backbone_unchanged = torch.equal( backbone_before, model.conv1.weight.detach(), ) head_changed = not torch.equal( head_before, model.fc.weight.detach(), ) torch.save( { "model_state_dict": model.state_dict(), "class_names": class_names, "weights": weights.name, }, "document-resnet18-transfer.pth", ) print(f"weights={weights}") print(f"classes={','.join(class_names)}") print(f"trainable_parameters={','.join(trainable_parameters)}") print(f"loss={loss_total / sample_count:.4f}") print(f"accuracy={correct / sample_count:.4f}") print(f"logits_shape={tuple(logits.shape)}") print(f"backbone_unchanged={backbone_unchanged}") print(f"head_changed={head_changed}") print("checkpoint=document-resnet18-transfer.pth")
The checkpoint keeps the class order and weight enum name beside the state dictionary so later inference can rebuild the same head and preprocessing.
Related: How to save and load a PyTorch model
Related: How to run inference in PyTorch
$ python transfer_learning_run.py weights=ResNet18_Weights.IMAGENET1K_V1 classes=invoice,receipt,statement trainable_parameters=fc.weight,fc.bias loss=1.1280 accuracy=0.3333 logits_shape=(4, 3) backbone_unchanged=True head_changed=True checkpoint=document-resnet18-transfer.pth
A first run may download the selected pretrained weights before printing the metrics. The exact loss and accuracy depend on the image data, while the trainable-parameter list, output width, unchanged backbone, changed head, and saved checkpoint prove the transfer-learning path.