Open In App

How to implement Genetic Algorithm using PyTorch

The optimization algorithms are capable of solving complex problems and genetic algorithm is one of the optimization algorithm. Genetic Algorithm can be easily integrate with PyTorch to address a wide array of optimization tasks. We will understand how to implement Genetic Algorithm using PyTorch.

Genetic Algorithms

Genetic algorithms (GAs) are optimization techniques inspired by natural selection. They start with a population of potential solutions to a problem and improve them over many generations. Genetic algorithms imitate natural selection to solve problems. They start with a group of solutions, evaluate how good they are, and then mix and mutate the best ones to create new solutions. This process continues, improving the solutions over many iterations until a satisfactory one is found. GAs are useful for complex problems where traditional methods struggle.

Algorithm:

Implementing Genetic Algorithm using PyTorch

Here, we implement a simple genetic algorithm (GA) to optimize the hyperparameters of a neural network using PyTorch. We create an initial population of individuals representing different sets of hyperparameters, evaluate their fitness by training and evaluating neural networks, perform selection, crossover, and mutation operations, and iterate for a fixed number of generations. For this we follow these steps:

Step 1: Import necessary libraries

We will first import necessary libraries:

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

Step 2: Define neural network

The CNN class defines a Convolutional Neural Network architecture using PyTorch's nn.Module. It consists of two convolutional layers (conv1 and conv2) followed by max-pooling operations and two fully connected layers (fc1 and fc2).

class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1)
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1)
self.fc1 = nn.Linear(64 * 7 * 7, 128)
self.fc2 = nn.Linear(128, 10)

def forward(self, x):
x = torch.relu(self.conv1(x))
x = torch.max_pool2d(x, kernel_size=2, stride=2)
x = torch.relu(self.conv2(x))
x = torch.max_pool2d(x, kernel_size=2, stride=2)
x = x.view(-1, 64 * 7 * 7)
x = torch.relu(self.fc1(x))
x = self.fc2(x)
return torch.log_softmax(x, dim=1)

Step 3: Compute Fitness

The function calculates the accuracy of a given model (individual) on the test dataset. It trains the model on the training dataset for a fixed number of epochs (5 in this case) and then evaluates its accuracy on the test dataset.

def compute_fitness(model, train_loader, test_loader):
criterion = nn.NLLLoss()
optimizer = optim.Adam(model.parameters())

model.train()
for epoch in range(5):
for data, target in train_loader:
optimizer.zero_grad()
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()

model.eval()
correct = 0
total = 0
with torch.no_grad():
for data, target in test_loader:
output = model(data)
_, predicted = torch.max(output.data, 1)
total += target.size(0)
correct += (predicted == target).sum().item()

accuracy = correct / total
return accuracy

Step 4: Define Genetic Algorithm Parameters

# Genetic algorithm parameters
population_size = 10
mutation_rate = 0.1
num_generations = 5

Step 5: Population Initialization Function

The function initializes the population with a specified number of CNN models.

# Initialize genetic algorithm parameters
def initialize_population():
population = []
for _ in range(population_size):
model = CNN()
population.append(model)
return population

Step 6: Crossover Operator (crossover):

The crossover operator combines genetic information from two parent models to produce two child models. It implements single-point crossover by swapping weights between the parent models' convolutional layers.

# Crossover operator: Single-point crossover
def crossover(parent1, parent2):
child1 = CNN()
child2 = CNN()
child1.conv1.weight.data = torch.cat((parent1.conv1.weight.data[:16], parent2.conv1.weight.data[16:]), dim=0)
child2.conv1.weight.data = torch.cat((parent2.conv1.weight.data[:16], parent1.conv1.weight.data[16:]), dim=0)
return child1, child2

Step 7: Mutation Operator (mutate):

The mutation operator introduces random perturbations to the parameters of the model with a certain probability (mutation_rate). It adds Gaussian noise to the model's parameters.

# Mutation operator: Random mutation
def mutate(model):
for param in model.parameters():
if torch.rand(1).item() < mutation_rate:
param.data += torch.randn_like(param.data) * 0.1 # Adding Gaussian noise with std=0.1
return model

Step 8: Loading Dataset

The MNIST dataset is loaded using torchvision. It consists of handwritten digit images and corresponding labels.

# Load MNIST dataset
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])
train_dataset = datasets.MNIST(root='./data', train=True, transform=transform, download=True)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_dataset = datasets.MNIST(root='./data', train=False, transform=transform, download=True)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)

Step 9: Executing Genetic Algorithm

The genetic algorithm iterates over multiple generations, evaluating the fitness of each individual (model) in the population, selecting the top-performing individuals for the next generation, and applying crossover and mutation operators to generate new individuals.

# Genetic algorithm
population = initialize_population()
for generation in range(num_generations):
print("Generation:", generation + 1)
best_accuracy = 0
best_individual = None

# Compute fitness for each individual
for individual in population:
fitness = compute_fitness(individual, train_loader, test_loader)
if fitness > best_accuracy:
best_accuracy = fitness
best_individual = individual

print("Best accuracy in generation", generation + 1, ":", best_accuracy)
print("Best individual:", best_individual)

next_generation = []

# Select top individuals for next generation
selected_individuals = population[:population_size // 2]

# Crossover and mutation
for i in range(0, len(selected_individuals), 2):
parent1 = selected_individuals[i]
parent2 = selected_individuals[i + 1]
child1, child2 = crossover(parent1, parent2)
child1 = mutate(child1)
child2 = mutate(child2)
next_generation.extend([child1, child2])

population = next_generation

Complete Code

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

# Define the neural network architecture (CNN)
class CNN(nn.Module):
    def __init__(self):
        super(CNN, self).__init__()
        self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1)
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1)
        self.fc1 = nn.Linear(64 * 7 * 7, 128)
        self.fc2 = nn.Linear(128, 10)

    def forward(self, x):
        x = torch.relu(self.conv1(x))
        x = torch.max_pool2d(x, kernel_size=2, stride=2)
        x = torch.relu(self.conv2(x))
        x = torch.max_pool2d(x, kernel_size=2, stride=2)
        x = x.view(-1, 64 * 7 * 7)
        x = torch.relu(self.fc1(x))
        x = self.fc2(x)
        return torch.log_softmax(x, dim=1)

# Function to compute fitness (accuracy) of an individual (hyperparameters)
def compute_fitness(model, train_loader, test_loader):
    criterion = nn.NLLLoss()
    optimizer = optim.Adam(model.parameters())

    model.train()
    for epoch in range(5):
        for data, target in train_loader:
            optimizer.zero_grad()
            output = model(data)
            loss = criterion(output, target)
            loss.backward()
            optimizer.step()

    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for data, target in test_loader:
            output = model(data)
            _, predicted = torch.max(output.data, 1)
            total += target.size(0)
            correct += (predicted == target).sum().item()

    accuracy = correct / total
    return accuracy

# Genetic algorithm parameters
population_size = 10
mutation_rate = 0.1
num_generations = 5

# Initialize genetic algorithm parameters
def initialize_population():
    population = []
    for _ in range(population_size):
        model = CNN()
        population.append(model)
    return population

# Crossover operator: Single-point crossover
def crossover(parent1, parent2):
    child1 = CNN()
    child2 = CNN()
    child1.conv1.weight.data = torch.cat((parent1.conv1.weight.data[:16], parent2.conv1.weight.data[16:]), dim=0)
    child2.conv1.weight.data = torch.cat((parent2.conv1.weight.data[:16], parent1.conv1.weight.data[16:]), dim=0)
    return child1, child2

# Mutation operator: Random mutation
def mutate(model):
    for param in model.parameters():
        if torch.rand(1).item() < mutation_rate:
            param.data += torch.randn_like(param.data) * 0.1  # Adding Gaussian noise with std=0.1
    return model

# Load MNIST dataset
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])
train_dataset = datasets.MNIST(root='./data', train=True, transform=transform, download=True)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_dataset = datasets.MNIST(root='./data', train=False, transform=transform, download=True)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)

# Genetic algorithm
population = initialize_population()
for generation in range(num_generations):
    print("Generation:", generation + 1)
    best_accuracy = 0
    best_individual = None

    # Compute fitness for each individual
    for individual in population:
        fitness = compute_fitness(individual, train_loader, test_loader)
        if fitness > best_accuracy:
            best_accuracy = fitness
            best_individual = individual

    print("Best accuracy in generation", generation + 1, ":", best_accuracy)
    print("Best individual:", best_individual)

    next_generation = []

    # Select top individuals for next generation
    selected_individuals = population[:population_size // 2]

    # Crossover and mutation
    for i in range(0, len(selected_individuals), 2):
        parent1 = selected_individuals[i]
        parent2 = selected_individuals[i + 1]
        child1, child2 = crossover(parent1, parent2)
        child1 = mutate(child1)
        child2 = mutate(child2)
        next_generation.extend([child1, child2])

    population = next_generation

# Print final population
print("Final population:")
for individual in population:
    print("Individual:", individual)

Output:

Generation: 1
Best accuracy in generation 1 : 0.912
Best individual: CNN(
(conv1): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv2): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(fc1): Linear(in_features=3136, out_features=128, bias=True)
(fc2): Linear(in_features=128, out_features=10, bias=True)
)
...

Generation: 5
Best accuracy in generation 5 : 0.935
Best individual: CNN(
(conv1): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv2): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(fc1): Linear(in_features=3136, out_features=128, bias=True)
(fc2): Linear(in_features=128, out_features=10, bias=True)
)

Final population:
Individual: CNN(
(conv1): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(conv2): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(fc1): Linear(in_features=3136, out_features=128, bias=True)
(fc2): Linear(in_features=128, out_features=10, bias=True)
)
...

Output Explanation

Conclusion

In summary, combining PyTorch with genetic algorithms or optimization methods offers a powerful way to solve complex problems in machine learning and beyond. By leveraging PyTorch's flexibility and genetic algorithms' exploration abilities, practitioners can efficiently optimize neural network architectures, fine-tune hyperparameters, defend against attacks, allocate resources effectively, and automate machine learning processes. This collaboration between PyTorch and genetic algorithms promises to drive innovation and solve challenging optimization problems across various domains.

Article Tags :