xbe.at

Machine learning / Advanced ML topics

Explaining Pretraining, Finetuning, and Transfer Learning in Machine Learning

12 min read, 15 sections

Contents
  1. Pretraining, Finetuning, and Transfer Learning
  2. Pretraining: Building a Foundation
  3. Finetuning: Adapting to Specific Tasks
  4. Transfer Learning: Leveraging Knowledge Across Domains
  5. Key Differences: Pretraining vs Finetuning vs Transfer Learning
  6. Pretraining: Advantages and Use Cases
  7. Finetuning: Techniques and Best Practices
  8. Transfer Learning: Strategies and Applications
  9. Real-life Example: Image Classification
  10. Real-life Example: Sentiment Analysis
  11. Challenges and Considerations
  12. Mitigating Challenges in Transfer Learning
  13. Future Directions in Transfer Learning
  14. Conclusion and Best Practices
  15. Additional Resources

Pretraining, Finetuning, and Transfer Learning

Pretraining, finetuning, and transfer learning are essential techniques in machine learning, particularly in deep learning. These methods allow us to leverage knowledge from one task or domain to improve performance on another, often with less data and computational resources. In this presentation, we'll explore each concept, their differences, and how they're implemented using Python.

import torch
import torch.nn as nn
import torchvision.models as models

# Load a pretrained ResNet model
pretrained_model = models.resnet18(pretrained=True)

# Freeze the parameters of the pretrained model
for param in pretrained_model.parameters():
    param.requires_grad = False

# Modify the last layer for a new task (e.g., binary classification)
num_ftrs = pretrained_model.fc.in_features
pretrained_model.fc = nn.Linear(num_ftrs, 2)

print(pretrained_model)

Pretraining: Building a Foundation

Pretraining involves training a model on a large dataset to learn general features and patterns. This process creates a foundation of knowledge that can be applied to various related tasks. Pretraining is often done on massive datasets, such as ImageNet for computer vision or large text corpora for natural language processing.

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms

# Define a simple CNN model
class SimpleCNN(nn.Module):
    def __init__(self):
        super(SimpleCNN, self).__init__()
        self.conv1 = nn.Conv2d(3, 16, 3, padding=1)
        self.conv2 = nn.Conv2d(16, 32, 3, padding=1)
        self.fc = nn.Linear(32 * 8 * 8, 10)

    def forward(self, x):
        x = torch.relu(self.conv1(x))
        x = torch.max_pool2d(x, 2)
        x = torch.relu(self.conv2(x))
        x = torch.max_pool2d(x, 2)
        x = x.view(-1, 32 * 8 * 8)
        x = self.fc(x)
        return x

# Pretrain on CIFAR-10
model = SimpleCNN()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters())

# Load CIFAR-10 dataset
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
trainset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)

# Train for one epoch (in practice, you'd train for many epochs)
for images, labels in trainloader:
    optimizer.zero_grad()
    outputs = model(images)
    loss = criterion(outputs, labels)
    loss.backward()
    optimizer.step()

print("Pretraining completed")

Finetuning: Adapting to Specific Tasks

Finetuning involves taking a pretrained model and adjusting its parameters for a specific task or dataset. This process allows the model to adapt its learned features to the new task while retaining the knowledge gained during pretraining. Finetuning is particularly useful when working with limited data for the target task.

# Continuing from the previous slide
# Assume we have a new dataset for a binary classification task
new_dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
new_dataset.targets = [1 if label in [0, 1, 2, 3, 4] else 0 for label in new_dataset.targets]  # Binary classification
new_dataloader = torch.utils.data.DataLoader(new_dataset, batch_size=64, shuffle=True)

# Modify the last layer for binary classification
model.fc = nn.Linear(32 * 8 * 8, 2)

# Finetune the model
optimizer = optim.Adam(model.parameters(), lr=0.0001)  # Lower learning rate for finetuning
criterion = nn.CrossEntropyLoss()

for epoch in range(5):  # Finetune for 5 epochs
    for images, labels in new_dataloader:
        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

print("Finetuning completed")

Transfer Learning: Leveraging Knowledge Across Domains

Transfer learning is a broader concept that encompasses both pretraining and finetuning. It involves applying knowledge gained from one task to improve performance on a different but related task. Transfer learning can be particularly effective when the source and target tasks share similar features or patterns.

import torch.nn.functional as F

# Define a new task: Emotion recognition from text
class TextClassifier(nn.Module):
    def __init__(self, pretrained_embedding):
        super(TextClassifier, self).__init__()
        self.embedding = pretrained_embedding
        self.lstm = nn.LSTM(300, 128, bidirectional=True, batch_first=True)
        self.fc = nn.Linear(256, 6)  # 6 basic emotions

    def forward(self, x):
        x = self.embedding(x)
        x, _ = self.lstm(x)
        x = x[:, -1, :]  # Take the last output
        x = self.fc(x)
        return F.log_softmax(x, dim=1)

# Load pretrained word embeddings (e.g., GloVe)
pretrained_embedding = nn.Embedding.from_pretrained(torch.randn(10000, 300))  # Simulated embeddings

# Create and train the model
model = TextClassifier(pretrained_embedding)
criterion = nn.NLLLoss()
optimizer = optim.Adam(model.parameters())

# Simulated training data
input_data = torch.randint(0, 10000, (100, 20))  # 100 sentences, max length 20
target_data = torch.randint(0, 6, (100,))  # 6 emotion classes

# Train for one epoch
for i in range(100):
    optimizer.zero_grad()
    output = model(input_data[i].unsqueeze(0))
    loss = criterion(output, target_data[i].unsqueeze(0))
    loss.backward()
    optimizer.step()

print("Transfer learning completed")

Key Differences: Pretraining vs Finetuning vs Transfer Learning

Pretraining focuses on learning general features from a large dataset, often in an unsupervised or self-supervised manner. Finetuning adapts a pretrained model to a specific task by updating its parameters on a smaller, task-specific dataset. Transfer learning is a broader concept that includes both pretraining and finetuning, as well as other techniques for leveraging knowledge across domains.

import matplotlib.pyplot as plt
import numpy as np

# Simulate learning curves
def learning_curve(method):
    x = np.linspace(0, 100, 100)
    if method == 'Pretraining':
        return 1 - np.exp(-x / 50)
    elif method == 'Finetuning':
        return 1 - 0.5 * np.exp(-x / 20)
    else:  # Transfer Learning
        return 1 - 0.3 * np.exp(-x / 10)

methods = ['Pretraining', 'Finetuning', 'Transfer Learning']
plt.figure(figsize=(10, 6))
for method in methods:
    y = learning_curve(method)
    plt.plot(y, label=method)

plt.xlabel('Training Iterations')
plt.ylabel('Performance')
plt.title('Learning Curves: Pretraining vs Finetuning vs Transfer Learning')
plt.legend()
plt.grid(True)
plt.show()

Pretraining: Advantages and Use Cases

Pretraining allows models to learn general features and representations from large datasets. This approach is particularly useful when working with complex data types like images or natural language. Pretrained models can serve as strong starting points for various downstream tasks, often leading to improved performance and faster convergence.

import torch
import torchvision.models as models
from torchvision import transforms
from PIL import Image

# Load a pretrained ResNet model
model = models.resnet50(pretrained=True)
model.eval()

# Prepare an image for inference
image_path = "example_image.jpg"
input_image = Image.open(image_path)
preprocess = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
input_tensor = preprocess(input_image)
input_batch = input_tensor.unsqueeze(0)

# Perform inference
with torch.no_grad():
    output = model(input_batch)

# Get the predicted class
_, predicted_idx = torch.max(output, 1)
print(f"Predicted class index: {predicted_idx.item()}")

Finetuning: Techniques and Best Practices

Finetuning involves carefully adjusting the parameters of a pretrained model to perform well on a new, specific task. Common techniques include freezing certain layers, using a lower learning rate, and gradually unfreezing layers during training. The choice of which layers to finetune depends on the similarity between the source and target tasks.

import torch.nn as nn
import torch.optim as optim

# Load a pretrained model
pretrained_model = models.resnet18(pretrained=True)

# Freeze all layers except the last few
for param in pretrained_model.parameters():
    param.requires_grad = False

# Replace the last fully connected layer
num_ftrs = pretrained_model.fc.in_features
pretrained_model.fc = nn.Linear(num_ftrs, 10)  # 10 classes in the new task

# Define optimizer with different learning rates
optimizer = optim.SGD([
    {'params': pretrained_model.fc.parameters()},
    {'params': pretrained_model.layer4.parameters(), 'lr': 1e-4},
], lr=1e-3, momentum=0.9)

# Training loop (simplified)
criterion = nn.CrossEntropyLoss()
for epoch in range(num_epochs):
    for inputs, labels in dataloader:
        optimizer.zero_grad()
        outputs = pretrained_model(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

print("Finetuning completed")

Transfer Learning: Strategies and Applications

Transfer learning encompasses various strategies for leveraging knowledge across domains. These include feature extraction, where pretrained layers are used as fixed feature extractors, and domain adaptation, where models are adjusted to perform well on a target domain with different statistical properties than the source domain.

import torch.nn as nn
import torch.optim as optim
from torchvision import models, transforms

# Load a pretrained model for feature extraction
feature_extractor = models.resnet18(pretrained=True)
feature_extractor = nn.Sequential(*list(feature_extractor.children())[:-1])
feature_extractor.eval()

# Define a new classifier
class NewClassifier(nn.Module):
    def __init__(self, num_features, num_classes):
        super(NewClassifier, self).__init__()
        self.fc = nn.Linear(num_features, num_classes)

    def forward(self, x):
        x = x.view(x.size(0), -1)
        return self.fc(x)

# Create and train the new classifier
new_classifier = NewClassifier(512, 5)  # 5 classes in the new task
optimizer = optim.Adam(new_classifier.parameters())
criterion = nn.CrossEntropyLoss()

# Training loop (simplified)
for epoch in range(num_epochs):
    for inputs, labels in dataloader:
        with torch.no_grad():
            features = feature_extractor(inputs)
        
        optimizer.zero_grad()
        outputs = new_classifier(features)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

print("Transfer learning completed")

Real-life Example: Image Classification

Let's consider a real-life example of using transfer learning for image classification. Imagine we want to build a system that can classify different types of vehicles. We can use a pretrained model on ImageNet and finetune it for our specific task.

import torch
import torchvision.models as models
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
from torchvision.datasets import ImageFolder

# Load pretrained ResNet model
model = models.resnet50(pretrained=True)

# Modify the last layer for our task (e.g., 5 types of vehicles)
num_ftrs = model.fc.in_features
model.fc = torch.nn.Linear(num_ftrs, 5)

# Define data transforms
data_transforms = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])

# Load and transform the dataset
dataset = ImageFolder(root='path/to/vehicle/dataset', transform=data_transforms)
dataloader = DataLoader(dataset, batch_size=32, shuffle=True)

# Define loss function and optimizer
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

# Training loop
num_epochs = 10
for epoch in range(num_epochs):
    for inputs, labels in dataloader:
        optimizer.zero_grad()
        outputs = model(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

print("Vehicle classification model trained")

Real-life Example: Sentiment Analysis

Another practical application of transfer learning is in natural language processing tasks like sentiment analysis. We can use a pretrained language model and finetune it for sentiment classification on product reviews.

import torch
from transformers import BertTokenizer, BertForSequenceClassification, AdamW

# Load pretrained BERT model and tokenizer
model = BertForSequenceClassification.from_pretrained('bert-base-uncased', num_labels=2)
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')

# Prepare the dataset (example)
texts = ["This product is amazing!", "I'm very disappointed with the quality."]
labels = [1, 0]  # 1 for positive, 0 for negative

# Tokenize and encode the texts
inputs = tokenizer(texts, padding=True, truncation=True, return_tensors="pt")
labels = torch.tensor(labels)

# Define optimizer
optimizer = AdamW(model.parameters(), lr=2e-5)

# Training loop
model.train()
for epoch in range(3):
    optimizer.zero_grad()
    outputs = model(**inputs, labels=labels)
    loss = outputs.loss
    loss.backward()
    optimizer.step()

# Inference
model.eval()
with torch.no_grad():
    new_text = "I love this new gadget!"
    new_input = tokenizer(new_text, return_tensors="pt")
    output = model(**new_input)
    prediction = torch.argmax(output.logits).item()

print(f"Sentiment: {'Positive' if prediction == 1 else 'Negative'}")

Challenges and Considerations

While pretraining, finetuning, and transfer learning are powerful techniques, they come with challenges. These include the risk of negative transfer (when knowledge from the source task harms performance on the target task), the need for careful hyperparameter tuning, and potential biases inherited from pretrained models. Additionally, computational resources required for pretraining large models can be substantial.

import numpy as np
import matplotlib.pyplot as plt

# Simulate the effect of negative transfer
def performance(transfer_similarity, epochs):
    base = 1 - np.exp(-epochs / 10)
    transfer_effect = transfer_similarity * (1 - np.exp(-epochs / 5))
    return base + transfer_effect

epochs = np.linspace(0, 50, 100)
similarities = [-0.5, 0, 0.5, 1]

plt.figure(figsize=(10, 6))
for sim in similarities:
    perf = performance(sim, epochs)
    plt.plot(epochs, perf, label=f'Similarity: {sim}')

plt.xlabel('Training Epochs')
plt.ylabel('Model Performance')
plt.title('Impact of Transfer Similarity on Model Performance')
plt.legend()
plt.grid(True)
plt.show()

Mitigating Challenges in Transfer Learning

To address challenges in transfer learning, researchers and practitioners employ various strategies. These include careful selection of source tasks, gradual unfreezing of layers during finetuning, and using techniques like domain adversarial training to reduce domain shift.

import torch
import torch.nn as nn

class GradualUnfreezeTrainer:
    def __init__(self, model, optimizer, criterion, num_epochs):
        self.model = model
        self.optimizer = optimizer
        self.criterion = criterion
        self.num_epochs = num_epochs

    def train(self, dataloader):
        for epoch in range(self.num_epochs):
            if epoch % 5 == 0 and epoch > 0:
                self._unfreeze_layer()
            
            for inputs, labels in dataloader:
                self.optimizer.zero_grad()
                outputs = self.model(inputs)
                loss = self.criterion(outputs, labels)
                loss.backward()
                self.optimizer.step()

    def _unfreeze_layer(self):
        for child in reversed(list(self.model.children())):
            if isinstance(child, nn.Sequential):
                for param in child.parameters():
                    param.requires_grad = True
                break

# Usage example (pseudo-code)
# model = create_pretrained_model()
# optimizer = create_optimizer(model.parameters())
# criterion = nn.CrossEntropyLoss()
# trainer = GradualUnfreezeTrainer(model, optimizer, criterion, num_epochs=20)
# trainer.train(dataloader)

Future Directions in Transfer Learning

The field of transfer learning is rapidly evolving, with new techniques and applications emerging. Some promising directions include meta-learning (learning to learn), few-shot learning, and continual learning. These approaches aim to create more flexible and adaptable models that can quickly learn new tasks with minimal data.

import torch
import torch.nn as nn
import torch.optim as optim

class MetaLearner(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(MetaLearner, self).__init__()
        self.network = nn.Sequential(
            nn.Linear(input_size, hidden_size),
            nn.ReLU(),
            nn.Linear(hidden_size, output_size)
        )
        self.meta_optimizer = optim.Adam(self.parameters(), lr=0.001)

    def forward(self, x):
        return self.network(x)

    def adapt(self, support_set, support_labels, num_inner_steps):
        inner_optimizer = optim.SGD(self.parameters(), lr=0.01)
        
        for _ in range(num_inner_steps):
            inner_loss = nn.functional.cross_entropy(self(support_set), support_labels)
            inner_optimizer.zero_grad()
            inner_loss.backward()
            inner_optimizer.step()

    def meta_learn(self, task_batch):
        meta_loss = 0
        for task in task_batch:
            support_set, support_labels, query_set, query_labels = task
            self.adapt(support_set, support_labels, num_inner_steps=5)
            meta_loss += nn.functional.cross_entropy(self(query_set), query_labels)
        
        self.meta_optimizer.zero_grad()
        meta_loss.backward()
        self.meta_optimizer.step()

# Usage example (pseudo-code)
# meta_learner = MetaLearner(input_size=10, hidden_size=50, output_size=5)
# for epoch in range(num_epochs):
#     task_batch = sample_tasks(num_tasks=16)
#     meta_learner.meta_learn(task_batch)

Conclusion and Best Practices

Pretraining, finetuning, and transfer learning are powerful techniques that have revolutionized machine learning. To make the most of these approaches:

  1. Choose appropriate pretrained models for your task.
  2. Consider the similarity between source and target domains.
  3. Experiment with different finetuning strategies.
  4. Be aware of potential biases in pretrained models.
  5. Monitor for overfitting, especially with small target datasets.
  6. Stay updated with the latest advancements in the field.
def transfer_learning_workflow(pretrained_model, target_dataset, num_epochs=10):
    # Freeze pretrained layers
    for param in pretrained_model.parameters():
        param.requires_grad = False
    
    # Add new layers for the target task
    num_ftrs = pretrained_model.fc.in_features
    pretrained_model.fc = nn.Linear(num_ftrs, target_dataset.num_classes)
    
    # Prepare data loaders
    train_loader, val_loader = prepare_data_loaders(target_dataset)
    
    # Define loss function and optimizer
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.Adam(pretrained_model.fc.parameters(), lr=0.001)
    
    # Training loop
    for epoch in range(num_epochs):
        train_one_epoch(pretrained_model, train_loader, criterion, optimizer)
        validate(pretrained_model, val_loader, criterion)
    
    return pretrained_model

# Usage example (pseudo-code)
# pretrained_model = load_pretrained_model()
# target_dataset = load_target_dataset()
# fine_tuned_model = transfer_learning_workflow(pretrained_model, target_dataset)

Additional Resources

For those interested in diving deeper into pretraining, finetuning, and transfer learning, here are some valuable resources:

  1. "Transfer Learning" by Sebastian Ruder, arXiv:1808.01974 https://arxiv.org/abs/1808.01974
  2. "A Survey on Transfer Learning" by Sinno Jialin Pan and Qiang Yang, arXiv:1808.01974 https://arxiv.org/abs/1810.03328
  3. "A Survey of Deep Transfer Learning" by Chuanqi Tan et al., arXiv:1808.01974 https://arxiv.org/abs/1808.01974

These papers provide comprehensive overviews of transfer learning techniques, applications, and challenges in various domains of machine learning.

More in Advanced ML topics

  1. Exploring Popular Distance Measures in Machine Learning8 min

    The Euclidean distance represents the shortest straight-line path between two points in a multi-dimensional space. It's calculated as the square root of the...

  2. Differential Geometry in Machine Learning with Python11 min

    Differential geometry provides a powerful framework for understanding and optimizing machine learning algorithms. It allows us to analyze the geometric...

  3. Exploring the Mathematics of Machine Learning Cost Functions10 min

    The cost function is a crucial component in machine learning, measuring how well a model's predictions align with actual values. Let's explore the...

  4. Differences Between Pretraining Finetuning and Transfer Learning11 min

    Pretraining is a fundamental technique in machine learning where a model is trained on a large dataset to learn general features and patterns before being...

  5. Exploring Transcendence in Machine Learning with Python9 min

    Transcendence in machine learning refers to the ability of AI systems to surpass human-level performance in specific tasks. This concept explores the...

  6. Diagnosing Data Deficiency in Machine Learning Models6 min

    Data deficiency in machine learning models can significantly impact performance. This technique helps determine if more data will improve your model or if...