> ## Documentation Index
> Fetch the complete documentation index at: https://ai.tharung.in/llms.txt
> Use this file to discover all available pages before exploring further.

# CIFAR 10

# Transfer Learning and Fine-Tuning with ResNet-18 on CIFAR-10

***

## 1. Learning Objectives

By completing this notebook, you will understand:

* How to load the CIFAR-10 dataset using `torchvision`
* How to preprocess images for a pretrained model
* How to load a pretrained ResNet-18
* How to replace the final classification layer
* How to freeze pretrained layers
* How to train only the new classifier
* How to unfreeze a pretrained block
* How to fine-tune a pretrained model
* How to evaluate classification accuracy
* How to inspect model predictions

### Key concepts

| Concept           | Meaning                                                 |
| ----------------- | ------------------------------------------------------- |
| Transfer Learning | Reusing knowledge learned by a pretrained model         |
| Freezing          | Preventing selected model parameters from being updated |
| Fine-Tuning       | Updating some pretrained layers for the new dataset     |
| Backbone          | The pretrained feature-extraction portion of the model  |
| Classifier        | The final layer that produces class predictions         |

***

# 2. Project Workflow

The complete workflow is:

```text theme={null}
CIFAR-10 Dataset
      ↓
Resize + Normalize
      ↓
Pretrained ResNet-18
      ↓
Replace Final Layer
      ↓
Freeze Backbone
      ↓
Train Final Classifier
      ↓
Unfreeze layer4
      ↓
Fine-Tune
      ↓
Evaluate
      ↓
Make Predictions
```

***

# 3. Import Libraries

We first import PyTorch, neural-network components, optimizers, datasets, transformations, pretrained models, and data loaders.

```python theme={null}
import torch
import torch.nn as nn
import torch.optim as optim

from torchvision import datasets, transforms, models
from torch.utils.data import DataLoader
```

### Explanation

* `torch` provides the main PyTorch functionality.
* `torch.nn` provides neural-network layers and loss functions.
* `torch.optim` provides optimizers such as Adam.
* `datasets` provides datasets such as CIFAR-10.
* `transforms` provides image preprocessing operations.
* `models` provides pretrained models such as ResNet-18.
* `DataLoader` loads data in batches during training.

***

# 4. Select the Device

PyTorch can train the model using either a GPU or CPU.

```python theme={null}
device = torch.device(
    "cuda" if torch.cuda.is_available() else "cpu"
)

print("Using:", device)
```

### Explanation

```python theme={null}
torch.cuda.is_available()
```

checks whether a CUDA-enabled NVIDIA GPU is available.

If CUDA is available:

```text theme={null}
cuda
```

is selected.

Otherwise:

```text theme={null}
cpu
```

is selected.

### Why this matters

GPU training is usually much faster for deep-learning models.

***

# 5. Define Image Transformations

The pretrained ResNet-18 expects images with a format similar to the images it was originally trained on.

```python theme={null}
transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize(
        [0.485, 0.456, 0.406],
        [0.229, 0.224, 0.225]
    )
])
```

### Explanation

### `Resize`

```python theme={null}
transforms.Resize((224, 224))
```

CIFAR-10 images are only:

```text theme={null}
32 × 32
```

We resize them to:

```text theme={null}
224 × 224
```

because ResNet-18 was designed around ImageNet-sized inputs.

### `ToTensor`

```python theme={null}
transforms.ToTensor()
```

converts the image into a PyTorch tensor.

It also converts pixel values into approximately:

```text theme={null}
0 → 1
```

### `Normalize`

```python theme={null}
transforms.Normalize(
    [0.485, 0.456, 0.406],
    [0.229, 0.224, 0.225]
)
```

normalizes the RGB channels using the mean and standard deviation commonly used for ImageNet-pretrained models.

### Simple idea

```text theme={null}
Raw Image
   ↓
Resize
   ↓
Tensor
   ↓
Normalize
   ↓
Model-ready Image
```

***

# 6. Load CIFAR-10

Now we download and load the training and testing datasets.

```python theme={null}
train_data = datasets.CIFAR10(
    "./data",
    train=True,
    download=True,
    transform=transform
)

test_data = datasets.CIFAR10(
    "./data",
    train=False,
    download=True,
    transform=transform
)
```

### Explanation

`CIFAR10` contains:

* 50,000 training images
* 10,000 test images
* 10 classes
* RGB color images

The 10 classes are:

```text theme={null}
airplane
automobile
bird
cat
deer
dog
frog
horse
ship
truck
```

### Important parameters

```python theme={null}
train=True
```

loads the training dataset.

```python theme={null}
train=False
```

loads the test dataset.

```python theme={null}
download=True
```

downloads the dataset automatically if it is not already available.

```python theme={null}
transform=transform
```

applies our preprocessing pipeline.

You do **not** need to manually download CIFAR-10.

The dataset will be stored in:

```text theme={null}
./data
```

***

# 7. Create DataLoaders

The dataset is now converted into batches using `DataLoader`.

```python theme={null}
train_loader = DataLoader(
    train_data,
    batch_size=64,
    shuffle=True
)

test_loader = DataLoader(
    test_data,
    batch_size=64
)
```

### Explanation

### `batch_size=64`

Instead of processing all images at once, the model processes:

```text theme={null}
64 images → model → update weights
```

at a time.

### `shuffle=True`

The training images are shuffled between epochs.

This helps prevent the model from learning based on the original ordering of the dataset.

### Test loader

We normally don't need to shuffle test data because we are only evaluating the model.

### Check dataset size

```python theme={null}
print("Train:", len(train_data))
print("Test:", len(test_data))
```

Expected output:

```text theme={null}
Train: 50000
Test: 10000
```

***

# 8. Load Pretrained ResNet-18

This is the main **transfer learning** step.

```python theme={null}
model = models.resnet18(weights="DEFAULT")
```

### What is ResNet-18?

ResNet-18 is a convolutional neural network containing residual blocks.

The pretrained model has already learned useful visual features such as:

```text theme={null}
Edges
  ↓
Textures
  ↓
Shapes
  ↓
Objects
```

Instead of training all these features from scratch, we reuse them.

### What does `weights="DEFAULT"` mean?

It loads pretrained weights rather than randomly initializing the network.

Conceptually:

```text theme={null}
ImageNet Training
       ↓
Pretrained ResNet-18
       ↓
Our CIFAR-10 Dataset
```

This is called **transfer learning**.

***

# 9. Understand the Original ResNet Output

The original ResNet-18 was trained on ImageNet.

ImageNet classification contains:

```text theme={null}
1000 classes
```

Therefore, the original final layer produces:

```text theme={null}
1000 outputs
```

But CIFAR-10 contains only:

```text theme={null}
10 classes
```

Therefore, we need to replace the final classification layer.

***

# 10. Replace the Final Layer

```python theme={null}
model.fc = nn.Linear(
    model.fc.in_features,
    10
)
```

### Explanation

The original layer is conceptually:

```text theme={null}
Features → 1000 classes
```

We replace it with:

```text theme={null}
Features → 10 classes
```

### `model.fc.in_features`

This gives us the number of input features expected by the original fully connected layer.

We reuse that value instead of manually specifying it.

### `10`

This represents the number of CIFAR-10 classes.

### New architecture

```text theme={null}
Input Image
     ↓
ResNet Feature Extractor
     ↓
Feature Vector
     ↓
New Fully Connected Layer
     ↓
10 CIFAR-10 Classes
```

***

# 11. Move the Model to the Device

```python theme={null}
model = model.to(device)
```

### Explanation

This moves the model to either:

```text theme={null}
GPU
```

or:

```text theme={null}
CPU
```

depending on the device selected earlier.

The input tensors must also be moved to the same device during training.

***

# 12. Freeze the Pretrained Layers

Initially, we don't want to modify the pretrained ResNet layers.

```python theme={null}
for param in model.parameters():
    param.requires_grad = False
```

### Explanation

`requires_grad=False` means:

> Do not calculate/update gradients for this parameter.

So the pretrained ResNet acts as a fixed feature extractor.

Conceptually:

```text theme={null}
Pretrained ResNet
       ↓
    FROZEN
       ↓
Feature Vector
       ↓
New Classifier
       ↓
    TRAINABLE
```

***

# 13. Unfreeze the Final Classifier

We need the new final layer to learn the CIFAR-10 classification task.

```python theme={null}
for param in model.fc.parameters():
    param.requires_grad = True
```

Now:

```text theme={null}
ResNet Backbone → Frozen
Final FC Layer  → Trainable
```

### Why?

The pretrained backbone already contains useful visual knowledge.

The new classifier has never seen CIFAR-10, so it needs to learn.

***

# 14. Define the Loss Function

For a multi-class classification problem, we use cross-entropy loss.

```python theme={null}
criterion = nn.CrossEntropyLoss()
```

### What does it do?

It compares:

```text theme={null}
Model prediction
        vs
Correct class
```

and produces a loss value.

For example:

```text theme={null}
Actual:     cat
Predicted:  dog

→ High loss
```

If the model predicts the correct class with high confidence:

```text theme={null}
Actual:     cat
Predicted:  cat

→ Low loss
```

***

# 15. Define the Optimizer

Initially, only the final classifier is trainable.

```python theme={null}
optimizer = optim.Adam(
    model.fc.parameters(),
    lr=0.001
)
```

### Explanation

Adam updates the trainable weights to reduce the loss.

The optimizer receives:

```python theme={null}
model.fc.parameters()
```

because only the final layer is currently trainable.

The learning rate is:

```python theme={null}
0.001
```

### Simple idea

```text theme={null}
Prediction
    ↓
Calculate Loss
    ↓
Backpropagation
    ↓
Adam
    ↓
Update FC Layer
```

***

# 16. Train the Final Classifier

We initially train for three epochs.

```python theme={null}
for epoch in range(3):

    model.train()

    correct = 0
    total = 0

    for images, labels in train_loader:

        images = images.to(device)
        labels = labels.to(device)

        optimizer.zero_grad()

        outputs = model(images)

        loss = criterion(outputs, labels)

        loss.backward()

        optimizer.step()

        predictions = outputs.argmax(1)

        total += labels.size(0)

        correct += (
            (predictions == labels)
            .sum()
            .item()
        )

    accuracy = 100 * correct / total

    print(
        f"Epoch {epoch + 1}: "
        f"Loss = {loss.item():.4f}, "
        f"Accuracy = {accuracy:.2f}%"
    )
```

***

## 16.1 Set Training Mode

```python theme={null}
model.train()
```

This tells PyTorch that the model is being used for training.

This is important for layers whose behavior changes between training and evaluation.

***

## 16.2 Reset Accuracy Counters

```python theme={null}
correct = 0
total = 0
```

These variables track how many predictions are correct.

***

## 16.3 Loop Through Batches

```python theme={null}
for images, labels in train_loader:
```

Each iteration provides:

```text theme={null}
images → input images
labels → correct classes
```

For example:

```text theme={null}
64 images
64 labels
```

because our batch size is 64.

***

## 16.4 Move Data to the Device

```python theme={null}
images = images.to(device)
labels = labels.to(device)
```

The data must be on the same device as the model.

***

## 16.5 Clear Previous Gradients

```python theme={null}
optimizer.zero_grad()
```

PyTorch accumulates gradients by default.

We clear the previous gradients before calculating new ones.

***

## 16.6 Forward Pass

```python theme={null}
outputs = model(images)
```

The images pass through ResNet.

Conceptually:

```text theme={null}
Images
  ↓
ResNet
  ↓
Features
  ↓
FC Layer
  ↓
10 Output Scores
```

The output shape is approximately:

```text theme={null}
[64, 10]
```

because:

```text theme={null}
64 images × 10 classes
```

***

## 16.7 Calculate Loss

```python theme={null}
loss = criterion(outputs, labels)
```

The loss measures how different the predictions are from the correct labels.

***

## 16.8 Backpropagation

```python theme={null}
loss.backward()
```

PyTorch calculates gradients for the trainable parameters.

Because the ResNet backbone is frozen:

```text theme={null}
Backbone → no parameter updates
FC layer → receives gradients
```

***

## 16.9 Update Parameters

```python theme={null}
optimizer.step()
```

Adam uses the calculated gradients to update the trainable weights.

***

## 16.10 Get Predictions

```python theme={null}
predictions = outputs.argmax(1)
```

The model produces 10 scores for every image.

For example:

```text theme={null}
airplane     0.02
automobile   0.01
bird         0.05
cat          0.80
deer         0.02
dog          0.04
frog         0.01
horse        0.02
ship         0.01
truck        0.02
```

`argmax(1)` selects the class with the highest score:

```text theme={null}
cat
```

***

## 16.11 Calculate Accuracy

```python theme={null}
total += labels.size(0)

correct += (
    (predictions == labels)
    .sum()
    .item()
)
```

For example:

```text theme={null}
Total predictions = 64
Correct predictions = 48
```

Accuracy:

```text theme={null}
48 / 64 × 100 = 75%
```

***

## 16.12 Print Training Results

```python theme={null}
accuracy = 100 * correct / total

print(
    f"Epoch {epoch + 1}: "
    f"Loss = {loss.item():.4f}, "
    f"Accuracy = {accuracy:.2f}%"
)
```

Example:

```text theme={null}
Epoch 1: Loss = 0.8502, Accuracy = 70.21%
Epoch 2: Loss = 0.7124, Accuracy = 75.48%
Epoch 3: Loss = 0.6811, Accuracy = 76.31%
```

The exact values will vary.

***

# 17. Fine-Tuning

Now we move from transfer learning to **fine-tuning**.

Initially:

```text theme={null}
All ResNet layers → Frozen
FC layer           → Trainable
```

Now we want some pretrained features to adapt to CIFAR-10.

***

# 18. Unfreeze `layer4`

```python theme={null}
for param in model.layer4.parameters():
    param.requires_grad = True
```

### Explanation

ResNet-18 contains several major layers:

```text theme={null}
conv1
layer1
layer2
layer3
layer4
fc
```

We keep the earlier layers frozen and only unfreeze:

```text theme={null}
layer4
```

### Why `layer4`?

Earlier layers generally learn more generic features:

```text theme={null}
Edges
Textures
Basic shapes
```

Later layers learn more task-specific visual features:

```text theme={null}
Object parts
Complex shapes
Higher-level patterns
```

Therefore, adapting the final ResNet block is a simple and efficient fine-tuning strategy.

***

# 19. Create a Smaller Learning Rate

```python theme={null}
optimizer = optim.Adam(
    filter(
        lambda p: p.requires_grad,
        model.parameters()
    ),
    lr=0.0001
)
```

### Explanation

The learning rate is reduced from:

```text theme={null}
0.001
```

to:

```text theme={null}
0.0001
```

This is important because the pretrained layers already contain useful knowledge.

We don't want to make large updates that could destroy the learned features.

### Trainable parameters

At this point:

```text theme={null}
layer4 → Trainable
fc     → Trainable

Everything else → Frozen
```

The `filter()` expression selects only parameters where:

```python theme={null}
p.requires_grad == True
```

***

# 20. Fine-Tune the Model

We train for two additional epochs.

```python theme={null}
for epoch in range(2):

    model.train()

    for images, labels in train_loader:

        images = images.to(device)
        labels = labels.to(device)

        optimizer.zero_grad()

        outputs = model(images)

        loss = criterion(outputs, labels)

        loss.backward()

        optimizer.step()

    print(
        f"Fine-tuning Epoch {epoch + 1}: "
        f"Loss = {loss.item():.4f}"
    )
```

### What changed?

Previously:

```text theme={null}
Only FC layer learns
```

Now:

```text theme={null}
layer4 + FC learn
```

So the pretrained ResNet features can adapt to CIFAR-10.

***

# 21. Transfer Learning vs Fine-Tuning

The project contains both concepts.

### Transfer learning

```text theme={null}
Pretrained ResNet
       ↓
Freeze backbone
       ↓
Train new FC layer
```

We reuse knowledge from ImageNet.

### Fine-tuning

```text theme={null}
Pretrained ResNet
       ↓
Unfreeze layer4
       ↓
Train layer4 + FC
       ↓
Use smaller learning rate
```

We allow some pretrained features to adapt to the new task.

### Easy interview explanation

> **Transfer learning** means reusing a pretrained model for a new task, while **fine-tuning** means updating some of those pretrained layers so their features adapt to the new dataset.

***

# 22. Evaluate the Model

After training, we evaluate the model on the test dataset.

```python theme={null}
model.eval()

correct = 0
total = 0

with torch.no_grad():

    for images, labels in test_loader:

        images = images.to(device)
        labels = labels.to(device)

        outputs = model(images)

        predictions = outputs.argmax(1)

        total += labels.size(0)

        correct += (
            predictions == labels
        ).sum().item()

accuracy = 100 * correct / total

print(f"Test Accuracy: {accuracy:.2f}%")
```

***

# 23. Switch to Evaluation Mode

```python theme={null}
model.eval()
```

This tells PyTorch that we are evaluating rather than training.

It changes the behavior of certain layers such as:

* Dropout
* Batch Normalization

***

# 24. Disable Gradient Calculation

```python theme={null}
with torch.no_grad():
```

During testing, we don't need gradients.

This:

* reduces memory usage
* reduces computation
* makes inference faster

We only need predictions.

***

# 25. Calculate Test Predictions

```python theme={null}
outputs = model(images)

predictions = outputs.argmax(1)
```

The model produces scores for each of the 10 classes.

`argmax(1)` selects the class with the highest score.

***

# 26. Calculate Test Accuracy

```python theme={null}
total += labels.size(0)

correct += (
    predictions == labels
).sum().item()
```

Finally:

```python theme={null}
accuracy = 100 * correct / total
```

Example output:

```text theme={null}
Test Accuracy: 78.42%
```

The exact accuracy can vary depending on hardware, training time, PyTorch/torchvision versions, and training configuration.

***

# 27. Display Some Predictions

We can inspect actual and predicted classes.

```python theme={null}
classes = [
    "airplane",
    "automobile",
    "bird",
    "cat",
    "deer",
    "dog",
    "frog",
    "horse",
    "ship",
    "truck"
]
```

These class names correspond to the CIFAR-10 labels.

***

# 28. Get a Batch of Test Images

```python theme={null}
images, labels = next(iter(test_loader))

images = images.to(device)
```

### Explanation

```python theme={null}
iter(test_loader)
```

creates an iterator over the test batches.

```python theme={null}
next(...)
```

gets the first batch.

So we obtain:

```text theme={null}
images → batch of test images
labels → corresponding actual classes
```

***

# 29. Generate Predictions

```python theme={null}
with torch.no_grad():

    outputs = model(images)

    predictions = outputs.argmax(1)
```

The model predicts the class for each image.

***

# 30. Print Actual vs Predicted

```python theme={null}
for i in range(10):

    print(
        f"Actual: {classes[labels[i]]:10s} "
        f"Predicted: {classes[predictions[i].item()]}"
    )
```

Example:

```text theme={null}
Actual: cat        Predicted: cat
Actual: airplane   Predicted: airplane
Actual: dog        Predicted: dog
Actual: truck      Predicted: automobile
Actual: frog       Predicted: frog
Actual: horse      Predicted: horse
Actual: ship       Predicted: ship
Actual: bird       Predicted: bird
Actual: deer       Predicted: deer
Actual: cat        Predicted: dog
```

This lets us quickly see how the model is performing on individual examples.

***

# 31. Complete Code

The following is the complete beginner-friendly version that can be placed into a single notebook.

```python theme={null}
import torch
import torch.nn as nn
import torch.optim as optim

from torchvision import datasets, transforms, models
from torch.utils.data import DataLoader


# --------------------------------------------------
# 1. Device
# --------------------------------------------------

device = torch.device(
    "cuda" if torch.cuda.is_available() else "cpu"
)

print("Using:", device)


# --------------------------------------------------
# 2. Transform
# --------------------------------------------------

transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize(
        [0.485, 0.456, 0.406],
        [0.229, 0.224, 0.225]
    )
])


# --------------------------------------------------
# 3. Dataset
# --------------------------------------------------

train_data = datasets.CIFAR10(
    "./data",
    train=True,
    download=True,
    transform=transform
)

test_data = datasets.CIFAR10(
    "./data",
    train=False,
    download=True,
    transform=transform
)


# --------------------------------------------------
# 4. DataLoader
# --------------------------------------------------

train_loader = DataLoader(
    train_data,
    batch_size=64,
    shuffle=True
)

test_loader = DataLoader(
    test_data,
    batch_size=64
)

print("Train:", len(train_data))
print("Test:", len(test_data))


# --------------------------------------------------
# 5. Pretrained ResNet-18
# --------------------------------------------------

model = models.resnet18(weights="DEFAULT")


# --------------------------------------------------
# 6. Replace classifier
# --------------------------------------------------

model.fc = nn.Linear(
    model.fc.in_features,
    10
)

model = model.to(device)


# --------------------------------------------------
# 7. Freeze pretrained layers
# --------------------------------------------------

for param in model.parameters():
    param.requires_grad = False

for param in model.fc.parameters():
    param.requires_grad = True


# --------------------------------------------------
# 8. Loss and optimizer
# --------------------------------------------------

criterion = nn.CrossEntropyLoss()

optimizer = optim.Adam(
    model.fc.parameters(),
    lr=0.001
)


# --------------------------------------------------
# 9. Train classifier
# --------------------------------------------------

for epoch in range(3):

    model.train()

    correct = 0
    total = 0

    for images, labels in train_loader:

        images = images.to(device)
        labels = labels.to(device)

        optimizer.zero_grad()

        outputs = model(images)

        loss = criterion(outputs, labels)

        loss.backward()

        optimizer.step()

        predictions = outputs.argmax(1)

        total += labels.size(0)

        correct += (
            predictions == labels
        ).sum().item()

    accuracy = 100 * correct / total

    print(
        f"Epoch {epoch + 1}: "
        f"Loss = {loss.item():.4f}, "
        f"Accuracy = {accuracy:.2f}%"
    )


# --------------------------------------------------
# 10. Fine-tune layer4
# --------------------------------------------------

for param in model.layer4.parameters():
    param.requires_grad = True


# --------------------------------------------------
# 11. Smaller learning rate
# --------------------------------------------------

optimizer = optim.Adam(
    filter(
        lambda p: p.requires_grad,
        model.parameters()
    ),
    lr=0.0001
)


# --------------------------------------------------
# 12. Fine-tuning
# --------------------------------------------------

for epoch in range(2):

    model.train()

    for images, labels in train_loader:

        images = images.to(device)
        labels = labels.to(device)

        optimizer.zero_grad()

        outputs = model(images)

        loss = criterion(outputs, labels)

        loss.backward()

        optimizer.step()

    print(
        f"Fine-tuning Epoch {epoch + 1}: "
        f"Loss = {loss.item():.4f}"
    )


# --------------------------------------------------
# 13. Evaluation
# --------------------------------------------------

model.eval()

correct = 0
total = 0

with torch.no_grad():

    for images, labels in test_loader:

        images = images.to(device)
        labels = labels.to(device)

        outputs = model(images)

        predictions = outputs.argmax(1)

        total += labels.size(0)

        correct += (
            predictions == labels
        ).sum().item()


accuracy = 100 * correct / total

print(f"Test Accuracy: {accuracy:.2f}%")


# --------------------------------------------------
# 14. Predictions
# --------------------------------------------------

classes = [
    "airplane",
    "automobile",
    "bird",
    "cat",
    "deer",
    "dog",
    "frog",
    "horse",
    "ship",
    "truck"
]

images, labels = next(iter(test_loader))

images = images.to(device)

with torch.no_grad():

    outputs = model(images)

    predictions = outputs.argmax(1)


for i in range(10):

    print(
        f"Actual: {classes[labels[i]]:10s} "
        f"Predicted: {classes[predictions[i].item()]}"
    )
```

***

# 32. What Happens During Prediction?

The complete prediction process is:

```text theme={null}
CIFAR-10 Image
      ↓
Resize to 224 × 224
      ↓
Normalize
      ↓
ResNet-18
      ↓
Extract visual features
      ↓
Fully Connected Layer
      ↓
10 class scores
      ↓
argmax()
      ↓
Predicted Class
```

For example:

```text theme={null}
Input:
Image of a dog

        ↓

ResNet-18

        ↓

Class scores:

dog        0.91
cat        0.04
horse      0.02
bird       0.01
...

        ↓

argmax()

        ↓

dog
```

***

# 33. Important Parameters to Remember

| Parameter          |       Value | Purpose                                |
| ------------------ | ----------: | -------------------------------------- |
| Image size         | `224 × 224` | Matches ResNet input preprocessing     |
| Batch size         |        `64` | Number of images per batch             |
| Classes            |        `10` | CIFAR-10 classes                       |
| Initial epochs     |         `3` | Train classifier                       |
| Fine-tuning epochs |         `2` | Adapt pretrained features              |
| Initial LR         |     `0.001` | Train new classifier                   |
| Fine-tuning LR     |    `0.0001` | Smaller updates to pretrained features |

***

# 34. Final Mental Model

Remember the project using this simple sequence:

```text theme={null}
1. Load CIFAR-10
       ↓
2. Resize + normalize images
       ↓
3. Load pretrained ResNet-18
       ↓
4. Replace 1000-class FC → 10-class FC
       ↓
5. Freeze pretrained layers
       ↓
6. Train new FC layer
       ↓
7. Unfreeze layer4
       ↓
8. Fine-tune with smaller LR
       ↓
9. Evaluate on test data
       ↓
10. Check predictions
```

## One-Sentence Summary

> **Use a pretrained ResNet-18 as a feature extractor, train a new CIFAR-10 classifier, then fine-tune the final ResNet block with a smaller learning rate.**
