Full Blog TOC

Full Blog Table Of Content with Keywords Available HERE

Thursday, August 22, 2024

Transfer Learning

 

In this post we have an example of transfer learning, a.k.a fine-tunning of a pre-trained model.

This entire code is simply a nice code version of the example in Transfer Learning for Computer Vision Tutorial.




import os
import time
from tempfile import TemporaryDirectory

import torch
import torch.backends.cudnn as cudnn
import torch.nn as nn
import torch.optim as optim
import torchvision
from torch.optim import lr_scheduler
from torchvision import datasets, transforms


class TransferLearning:
def __init__(self):
cudnn.benchmark = True
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

mean = [0.485, 0.456, 0.406]
std = [0.229, 0.224, 0.225]
image_size = 224
data_transforms = {
'train': transforms.Compose([
transforms.RandomResizedCrop(image_size),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize(mean, std)
]),
'val': transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(image_size),
transforms.ToTensor(),
transforms.Normalize(mean, std)
]),
}

# need to manually copy and extract the images from here:
# https://download.pytorch.org/tutorial/hymenoptera_data.zip
data_dir = 'hymenoptera_data'

image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),
data_transforms[x])
for x in ['train', 'val']}
self.dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=4,
shuffle=True, num_workers=4)
for x in ['train', 'val']}
self.dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'val']}
class_names = image_datasets['train'].classes
print('class names are:', class_names)

model_conv = torchvision.models.resnet18(weights='IMAGENET1K_V1')

print('original model')
print(model_conv)
print()

# freeze all model
for param in model_conv.parameters():
param.requires_grad = False

# replace the last classifier fully connected network - the only once to be trained
fully_connected_input_features = model_conv.fc.in_features
model_conv.fc = nn.Linear(fully_connected_input_features, len(class_names))

self.model = model_conv.to(self.device)

print('transform model')
print(model_conv)
print()

self.optimizer = optim.SGD(self.model.fc.parameters(), lr=0.001, momentum=0.9)
self.learning_rate_scheduler = lr_scheduler.StepLR(self.optimizer, step_size=7, gamma=0.1)

def train_model(self, num_epochs=3):
start_time = time.time()

# Create a temporary directory to save training checkpoints
with TemporaryDirectory() as tempdir:
best_model_params_path = os.path.join(tempdir, 'best_model_params.pt')

torch.save(self.model.state_dict(), best_model_params_path)
best_acc = 0.0

for epoch in range(num_epochs):
print(f'Epoch {epoch}/{num_epochs - 1}')
print('-' * 10)

self.model.train()
self.run_epoch('train')

self.model.eval()
epoch_acc = self.run_epoch('val')

if epoch_acc > best_acc:
best_acc = epoch_acc
torch.save(self.model.state_dict(), best_model_params_path)

print()

time_elapsed = time.time() - start_time
print(f'Training complete in {time_elapsed // 60:.0f}m {time_elapsed % 60:.0f}s')
print(f'Best validation Acc: {best_acc:4f}')

# load best model weights
self.model.load_state_dict(torch.load(best_model_params_path))

def run_epoch(self, phase):
running_loss = 0.0
running_corrects = 0

for inputs, labels in self.dataloaders[phase]:
inputs = inputs.to(self.device)
labels = labels.to(self.device)

# zero the parameter gradients
self.optimizer.zero_grad()

with torch.set_grad_enabled(phase == 'train'):
outputs = self.model(inputs)
_, predictions = torch.max(outputs, 1)
loss = nn.CrossEntropyLoss()(outputs, labels)

if phase == 'train':
loss.backward()
self.optimizer.step()

running_loss += loss.item() * inputs.size(0)
running_corrects += torch.sum(predictions == labels.data)
if phase == 'train':
self.learning_rate_scheduler.step()

epoch_loss = running_loss / self.dataset_sizes[phase]
epoch_acc = running_corrects.double() / self.dataset_sizes[phase]

print(f'{phase} Loss: {epoch_loss:.4f} Acc: {epoch_acc:.4f}')
return epoch_acc


def main():
transfer = TransferLearning()
transfer.train_model()


main()

Resnet32 Full Implementation in Pytorch

 

In this post we show a self implementaion of Resnet32 using pytorch.

The implementation is based on the following:


The Resnet32 arcitechture is contains a convolution first layer and a fully connected last layer.

The real trick for this network are the skip connections, which skip a layer in case its weight got too low values during backpropagation, and hence avoiding collapsing the entire chain.




The thing I've noticed only during implementation is that we have multiple layers handling the same input dimension. For example, we have 6 blocks handling the 28X28 input size. This consumes huge amount of parameters, though this is one of the smaller common networks.



image from https://www.researchgate.net/figure/Number-of-training-parameters-in-millionsM-for-VGG-ResNet-and-DenseNet-models_tbl1_338552250



As input database, we are using the CIFAR-10 dataset.


The code below is an object oriented based implementation.


import time

import matplotlib.pyplot as plt
import torch
import torchvision
from torch.utils.data import DataLoader
from torchvision import datasets


class ResnetBlock(torch.nn.Module):
def __init__(self, input_channels, output_channels, stride):
super(ResnetBlock, self).__init__()

# we could add Dropout2d(p=0.5) here to avoid overfitting

self.convolution_layer = torch.nn.Sequential(
torch.nn.Conv2d(input_channels, output_channels, kernel_size=3, stride=stride, padding=1, bias=False),
torch.nn.BatchNorm2d(output_channels),
torch.nn.ReLU(inplace=True),
torch.nn.Conv2d(output_channels, output_channels, kernel_size=3, stride=1, padding=1, bias=False),
torch.nn.BatchNorm2d(output_channels),
)
self.skip_layer = None
if stride != 1 or input_channels != output_channels:
self.skip_layer = torch.nn.Sequential(
torch.nn.Conv2d(input_channels, output_channels, kernel_size=1, stride=stride, bias=False),
torch.nn.BatchNorm2d(output_channels),
)
self.relu = torch.nn.ReLU(inplace=True)

def forward(self, x):
identity = x
z = self.convolution_layer(x)

if self.skip_layer:
identity = self.skip_layer(x)

out = z + identity
return self.relu(out)


class ResnetLayer(torch.nn.Module):
def __init__(self, input_channels, output_channels, stride, blocks_count):
super(ResnetLayer, self).__init__()
layers = [
ResnetBlock(input_channels, output_channels, stride),
]

for _ in range(blocks_count - 1):
layers.append(ResnetBlock(output_channels, output_channels, 1))

self.layers = torch.nn.Sequential(*layers)

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


class Resnet32(torch.nn.Module):

def __init__(self, number_of_classes):
super(Resnet32, self).__init__()

# 224 X 224

input_channels = 3
output_channels = 64
self.convolution1 = self.create_convolution1(input_channels, output_channels)

# 112 X 112

input_channels = output_channels
output_channels = 128
self.block1 = ResnetLayer(input_channels, output_channels, stride=1, blocks_count=3)

# 56 X 56

input_channels = output_channels
output_channels = 256
self.block2 = ResnetLayer(input_channels, output_channels, stride=2, blocks_count=4)

# 28 X 28

input_channels = output_channels
output_channels = 512
self.block3 = ResnetLayer(input_channels, output_channels, stride=2, blocks_count=6)

# 14 X 14

input_channels = output_channels
output_channels = 1024
self.block4 = ResnetLayer(input_channels, output_channels, stride=2, blocks_count=3)

# 7 X 7

self.classifier = self.create_classifier(output_channels, number_of_classes)

@staticmethod
def create_convolution1(input_channels, output_channels):
return torch.nn.Sequential(
torch.nn.Conv2d(input_channels, output_channels, kernel_size=7, stride=2, padding=3, bias=False),
torch.nn.BatchNorm2d(output_channels),
torch.nn.ReLU(inplace=True),
torch.nn.MaxPool2d(kernel_size=3, stride=2, padding=1),
)

@staticmethod
def create_classifier(number_of_channels, number_of_classes):
return torch.nn.Sequential(
torch.nn.AdaptiveAvgPool2d((1, 1)),
torch.nn.Flatten(),
torch.nn.Linear(number_of_channels, number_of_classes),
)

def forward(self, x):
x = self.convolution1(x)
x = self.block1(x)
x = self.block2(x)
x = self.block3(x)
x = self.block4(x)
x = self.classifier(x)
return x


class Trainer:

def __init__(self):
batch_size = 128
learning_rate = 0.1
learning_momentum = 0.9
learning_rate_scheduler_factor = 0.1

self.loss_train_per_batch = []

limit_size = None
limit_size = 1000
if limit_size is None:
sampler = None
shuffle = True
else:
sampler = torch.arange(limit_size)
shuffle = False

self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print('using device:', self.device)

scale_up = (250, 250)
crop_down = (224, 224)
mean = (0.5, 0.5, 0.5)
std = (0.5, 0.5, 0.5)

transform_train = torchvision.transforms.Compose(
[
torchvision.transforms.Resize(scale_up),
torchvision.transforms.RandomCrop(crop_down),
torchvision.transforms.RandomRotation(20),
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize(mean, std),
]
)
transform_test = torchvision.transforms.Compose(
[
torchvision.transforms.Resize(scale_up),
torchvision.transforms.CenterCrop(crop_down),
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize(mean, std),
]
)

dataset_train = datasets.CIFAR10(root='local_cache_folder',
train=True,
transform=transform_train,
download=True)

dataset_test = datasets.CIFAR10(root='local_cache_folder',
train=False,
transform=transform_test)

print('train samples', dataset_train.data.shape[0])
print('test samples', dataset_train.data.shape[0])

self.loader_train = DataLoader(dataset=dataset_train,
batch_size=batch_size,
shuffle=shuffle,
sampler=sampler,
)

self.loader_test = DataLoader(dataset=dataset_test,
batch_size=batch_size,
shuffle=False,
sampler=sampler,
)

for images, labels in self.loader_train:
print('single batch dimensions:', images.shape)
print('single batch label dimensions:', labels.shape)
self.number_of_features = images.shape[2] * images.shape[3]
print('number of features', self.number_of_features)
break

self.model = Resnet32(number_of_classes=10)
self.model = self.model.to(device=self.device)

self.optimizer = torch.optim.SGD(self.model.parameters(), lr=learning_rate, momentum=learning_momentum)

self.scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer=self.optimizer,
factor=learning_rate_scheduler_factor, mode='max')

self.loss_function = torch.nn.functional.cross_entropy

def run_batches(self, data_loader, batch_callback=None):
total_loss = 0
total_samples = 0
correct_predictions = 0
batches_in_epoch_count = 0
for _, (batch_x, batch_labels) in enumerate(data_loader):
batches_in_epoch_count += 1
batch_x = batch_x.to(device=self.device)
batch_labels = batch_labels.to(device=self.device)
batch_samples = batch_x.shape[0]
total_samples += batch_samples
logics = self.model(batch_x)
batch_loss = self.loss_function(logics, batch_labels)
batch_predictions = torch.argmax(logics, dim=1)
batch_correct = batch_predictions == batch_labels
correct_predictions += batch_correct.sum()
if batch_callback is not None:
batch_callback(logics, batch_loss, batch_samples)
total_loss += batch_loss

average_loss = total_loss / total_samples
accuracy = float(correct_predictions) / total_samples

return average_loss.cpu(), accuracy, batches_in_epoch_count

def batch_callback_train(self, _, batch_loss, batch_samples):
self.loss_train_per_batch.append(batch_loss.item() / batch_samples)

self.optimizer.zero_grad()
batch_loss.backward()
self.optimizer.step()

def train(self, number_of_epochs):
accuracy_train_per_epoch = []
accuracy_test_per_epoch = []
loss_train_per_epoch = []
loss_test_per_epoch = []
self.loss_train_per_batch = []
self.model.train()

for epoch_index in range(number_of_epochs):
start_time = time.time()
self.run_batches(data_loader=self.loader_train, batch_callback=self.batch_callback_train)

with torch.no_grad():
epoch_loss_train, epoch_accuracy_train, batches_in_epoch_count = self.run_batches(
data_loader=self.loader_train)
epoch_loss_test, epoch_accuracy_test, _ = self.run_batches(data_loader=self.loader_test)

loss_train_per_epoch.append(epoch_loss_train)
loss_test_per_epoch.append(epoch_loss_test)
accuracy_train_per_epoch.append(epoch_accuracy_train)
accuracy_test_per_epoch.append(epoch_accuracy_test)

self.scheduler.step(epoch_loss_train)

passed_seconds = time.time() - start_time
print(f'epoch {epoch_index},'
f'process seconds {passed_seconds},'
f'loss train {epoch_loss_train},'
f'loss test {epoch_loss_test},'
f'accuracy train {epoch_accuracy_train},'
f'accuracy test {epoch_accuracy_test}')

self.model.eval()

loss_train_per_epoch = self.spread_points(loss_train_per_epoch, batches_in_epoch_count)
loss_test_per_epoch = self.spread_points(loss_test_per_epoch, batches_in_epoch_count)
plt.clf()
plt.plot(self.loss_train_per_batch, color='b', label='train batch')
plt.plot(loss_train_per_epoch, color='g', label='train epoch')
plt.plot(loss_test_per_epoch, color='r', label='test')
plt.legend()
plt.ylim(0, 0.01)
plt.ylabel('Loss')
plt.xlabel('Epoch')
plt.savefig("loss.pdf")

plt.clf()
plt.plot(accuracy_train_per_epoch, color='b', label='train')
plt.plot(accuracy_test_per_epoch, color='r', label='test')
plt.legend()
plt.ylabel('Accuracy')
plt.xlabel('Epoch')
plt.savefig("accuracy.pdf")
self.loss_train_per_batch = []

@staticmethod
def spread_points(points, spread_factor):
result = []
for point in points:
for _ in range(spread_factor):
result.append(point)
return result


def main():
random_seed = 42
number_of_epochs = 10

torch.manual_seed(random_seed)
trainer = Trainer()
trainer.train(number_of_epochs)


main()




Training this network is very resources consuming, I run it on a CPU, with just 10 samples to check that it is not failing, and it took several minutes.

Running this on Google colabs also took hours.


Tuesday, August 20, 2024

Convolutional Network Usage

 

In this post contains an example of a convolutional network usage.


The network architecture is based on LeNet5, which is based on two major parts: convolution and classifier. 

The convolution part contains 2 layers of Conv2d and MaxPool2d.

The classifier part contains 2 layer of a fully connected neural network.


The dataset is the MNIST database.


We use SGD optimizer, along with learning rate momentum, which uses a moving average of the several last loss results to better converge to the minimum.

We use a learning rate scheduler to update the learning rate accoring to the accuracy.


import time

import matplotlib.pyplot as plt
import torch
import torchvision
from torch.utils.data import DataLoader
from torchvision import datasets


class LeNet5(torch.nn.Module):

def __init__(self, number_of_classes, gray_scale=True):
super(LeNet5, self).__init__()

if gray_scale:
in_channels = 1
else:
in_channels = 3

convolution_kernel_size = 5
pooling_kernel_size = 2
convolution_channels_layer1 = 6
convolution_channels_layer2 = 16

self.convolution = torch.nn.Sequential(

# we could also set the stride and padding
# reduce according to kernel size: 32X32 -> 28X28
torch.nn.Conv2d(in_channels=in_channels, out_channels=convolution_channels_layer1,
kernel_size=convolution_kernel_size),

# we could also use ReLU
torch.nn.Tanh(),

# reduce according to stride which is by default the kernel size 28X28 -> 14X14
torch.nn.MaxPool2d(kernel_size=pooling_kernel_size),

# reduce according to kernel size: 14X14 -> 10X10
torch.nn.Conv2d(in_channels=convolution_channels_layer1, out_channels=convolution_channels_layer2,
kernel_size=convolution_kernel_size),

torch.nn.Tanh(),

# reduce according to stride which is by default the kernel size 10X10 -> 5X5
torch.nn.MaxPool2d(kernel_size=pooling_kernel_size),
)

classifier_layer1_width = 120
classifier_layer2_width = 84

# we could also use dropout
self.classifier = torch.nn.Sequential(
torch.nn.Flatten(),
torch.nn.Linear(in_features=convolution_channels_layer2 * 5 * 5,
out_features=classifier_layer1_width),
torch.nn.Tanh(),
torch.nn.Linear(in_features=classifier_layer1_width, out_features=classifier_layer2_width),
torch.nn.Tanh(),
torch.nn.Linear(in_features=classifier_layer2_width, out_features=number_of_classes),
)

def forward(self, x):
z1 = self.convolution(x)
logits = self.classifier(z1)
return logits


class Trainer:

def __init__(self):
batch_size = 100
learning_rate = 0.1
learning_momentum = 0.9
learning_rate_scheduler_factor = 0.1

self.loss_train_per_batch = []

limit_size = None
limit_size = 10000
if limit_size is None:
sampler = None
shuffle = True
else:
sampler = torch.arange(limit_size)
shuffle = False

self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print('using device:', self.device)

# we could also use RandomCrop, RandomRotation, ...
resize_transform = torchvision.transforms.Compose(
[
torchvision.transforms.Resize((32, 32)),
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize((0.5,), (0.5,)),
]
)

dataset_train = datasets.MNIST(root='local_cache_folder',
train=True,
transform=resize_transform,
download=True)

dataset_test = datasets.MNIST(root='local_cache_folder',
train=False,
transform=resize_transform)

print('train samples', dataset_train.data.shape[0])
print('test samples', dataset_train.data.shape[0])

self.loader_train = DataLoader(dataset=dataset_train,
batch_size=batch_size,
shuffle=shuffle,
sampler=sampler,
)

self.loader_test = DataLoader(dataset=dataset_test,
batch_size=batch_size,
shuffle=False,
sampler=sampler,
)

for images, labels in self.loader_train:
print('single batch dimensions:', images.shape)
print('single batch label dimensions:', labels.shape)
self.number_of_features = images.shape[2] * images.shape[3]
print('number of features', self.number_of_features)
break

self.model = LeNet5(number_of_classes=10)
self.model = self.model.to(device=self.device)

self.optimizer = torch.optim.SGD(self.model.parameters(), lr=learning_rate, momentum=learning_momentum)

self.scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer=self.optimizer,
factor=learning_rate_scheduler_factor, mode='max')

self.loss_function = torch.nn.functional.cross_entropy

def run_batches(self, data_loader, batch_callback=None):
total_loss = 0
total_samples = 0
correct_predictions = 0
batches_in_epoch_count = 0
for _, (batch_x, batch_labels) in enumerate(data_loader):
batches_in_epoch_count += 1
# reshape from [100, 1, 28, 28] to [100, 28*28]
batch_x = batch_x.to(device=self.device)
batch_labels = batch_labels.to(device=self.device)
batch_samples = batch_x.shape[0]
total_samples += batch_samples
logics = self.model(batch_x)
batch_loss = self.loss_function(logics, batch_labels)
batch_predictions = torch.argmax(logics, dim=1)
batch_correct = batch_predictions == batch_labels
correct_predictions += batch_correct.sum()
if batch_callback is not None:
batch_callback(logics, batch_loss, batch_samples)
total_loss += batch_loss

average_loss = total_loss / total_samples
accuracy = float(correct_predictions) / total_samples

return average_loss.cpu(), accuracy, batches_in_epoch_count

def batch_callback_train(self, _, batch_loss, batch_samples):
self.loss_train_per_batch.append(batch_loss.item() / batch_samples)

self.optimizer.zero_grad()
batch_loss.backward()
self.optimizer.step()

def train(self, number_of_epochs):
accuracy_train_per_epoch = []
accuracy_test_per_epoch = []
loss_train_per_epoch = []
loss_test_per_epoch = []
self.loss_train_per_batch = []
self.model.train()

for epoch_index in range(number_of_epochs):
start_time = time.time()
self.run_batches(data_loader=self.loader_train, batch_callback=self.batch_callback_train)

with torch.no_grad():
epoch_loss_train, epoch_accuracy_train, batches_in_epoch_count = self.run_batches(
data_loader=self.loader_train)
epoch_loss_test, epoch_accuracy_test, _ = self.run_batches(data_loader=self.loader_test)

loss_train_per_epoch.append(epoch_loss_train)
loss_test_per_epoch.append(epoch_loss_test)
accuracy_train_per_epoch.append(epoch_accuracy_train)
accuracy_test_per_epoch.append(epoch_accuracy_test)

self.scheduler.step(epoch_loss_train)

passed_seconds = time.time() - start_time
print(f'epoch {epoch_index},'
f'process seconds {passed_seconds},'
f'loss train {epoch_loss_train},'
f'loss test {epoch_loss_test},'
f'accuracy train {epoch_accuracy_train},'
f'accuracy test {epoch_accuracy_test}')

self.model.eval()

loss_train_per_epoch = self.spread_points(loss_train_per_epoch, batches_in_epoch_count)
loss_test_per_epoch = self.spread_points(loss_test_per_epoch, batches_in_epoch_count)
plt.clf()
plt.plot(self.loss_train_per_batch, color='b', label='train batch')
plt.plot(loss_train_per_epoch, color='g', label='train epoch')
plt.plot(loss_test_per_epoch, color='r', label='test')
plt.legend()
plt.ylim(0, 0.01)
plt.ylabel('Loss')
plt.xlabel('Epoch')
plt.savefig("loss.pdf")

plt.clf()
plt.plot(accuracy_train_per_epoch, color='b', label='train')
plt.plot(accuracy_test_per_epoch, color='r', label='test')
plt.legend()
plt.ylabel('Accuracy')
plt.xlabel('Epoch')
plt.savefig("accuracy.pdf")
self.loss_train_per_batch = []

@staticmethod
def spread_points(points, spread_factor):
result = []
for point in points:
for _ in range(spread_factor):
result.append(point)
return result


def main():
random_seed = 42
number_of_epochs = 10

torch.manual_seed(random_seed)
trainer = Trainer()
trainer.train(number_of_epochs)


main()



The results for accuracy and lost are below.

The loss chart includes both loss per each batch, and a global loss upon epoch completion.


We can see an overfitting taking place. We could address this issue by:

1. Using a larger dataset (we used only 10K samples of the dataset)

2. Adding DropOut as part of the classifier part

3. Using augmentated input such as RandomCrop, RandomRotation.

4. Using BatchNorm









Saturday, August 17, 2024

Pytorch DataLoaders and Transformers


In this post we present an example of images data loader. We have both loading the images, and augmentation of them differently in each epoch. 


import pandas as pd
import torch
import torchvision
from PIL import Image
from torch.utils.data import DataLoader


class MyDataset(torch.utils.data.Dataset):
def __init__(self, csv_path, images_folder, transform=None):
self.images_folder = images_folder

df = pd.read_csv(csv_path)
self.images_names = df['file name']
self.images_labels = df['label']
self.transform = transform

def __getitem__(self, item_index):
image_path = self.images_folder + '/' + self.images_names[item_index]
image = Image.open(image_path)
if self.transform is not None:
image = self.transform(image)

label = self.images_labels[item_index]
return image, label

def __len__(self):
return self.images_names.shape[0]


def main():
# NOTICE: should use transformers also on the test/validation datasets

custom_transform = torchvision.transforms.Compose(
[
torchvision.transforms.Resize(size=(32, 32)),
torchvision.transforms.RandomCrop(size=(28, 28)),
torchvision.transforms.RandomRotation(degrees=30,
interpolation=torchvision.transforms.InterpolationMode.BILINEAR),
torchvision.transforms.ToTensor(),
# torchvision.transforms.Lambda(lambda item: item / 256.0),
# torchvision.transforms.Normalize(mean=(0.5,), std=(0.5,))
]
)
dataset = MyDataset(
csv_path='images.csv',
images_folder='images',
transform=custom_transform
)

data_loader = torch.utils.data.DataLoader(
dataset=dataset,
batch_size=100,
drop_last=False, # drop last batch
shuffle=True,
num_workers=1, # parallel data loading
)

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
number_of_epochs = 3
augmented_index = 0
for epoch_index in range(number_of_epochs):
print(f'epoch {epoch_index}')
for batch_index, (x, y) in enumerate(data_loader):
print(f'batch {batch_index}')
x = x.to(device)
y = y.to(device)
print(x.shape, y.shape)
augmented_image = x.cpu()
augmented_index += 1
torchvision.utils.save_image(augmented_image, f'augmented/{augmented_index}.png')


main()


The original images are:





And the augmented images per epoch are:










Tuesday, August 13, 2024

Multi-Layer Neural Network using Sequential on MNIST


from: https://en.wikipedia.org/wiki/MNIST_database


 This post displays a nice example of multi class classification using multi layered neural network. The database used is the MNIST digits images. This code uses object oriented design, and is readable, and friendly.



import time

import matplotlib.pyplot as plt
import torch
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision import transforms


class MultiLayerNetwork(torch.nn.Module):

def __init__(self, number_of_features, number_of_outputs):
super(MultiLayerNetwork, self).__init__()
self.number_of_features = number_of_features
self.loss_function = torch.nn.functional.cross_entropy
hidden_layer_width = 100
self.network = torch.nn.Sequential(
torch.nn.Linear(number_of_features, hidden_layer_width),
torch.nn.Sigmoid(),
torch.nn.Linear(hidden_layer_width, number_of_outputs),
)

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


class Trainer:

def __init__(self):
batch_size = 100
learning_rate = 0.1

limit_size = None
# limit_size = 1000
if limit_size is None:
sampler = None
shuffle = True
else:
sampler = torch.arange(limit_size)
shuffle = False

self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print('using device:', self.device)

dataset_train = datasets.MNIST(root='local_cache_folder',
train=True,
transform=transforms.ToTensor(),
download=True)

dataset_test = datasets.MNIST(root='local_cache_folder',
train=False,
transform=transforms.ToTensor())

print('train samples', dataset_train.data.shape[0])
print('test samples', dataset_train.data.shape[0])

self.loader_train = DataLoader(dataset=dataset_train,
batch_size=batch_size,
shuffle=shuffle,
sampler=sampler,
)

self.loader_test = DataLoader(dataset=dataset_test,
batch_size=batch_size,
shuffle=False,
sampler=sampler,
)

for images, labels in self.loader_train:
print('single batch dimensions:', images.shape)
print('single batch label dimensions:', labels.shape)
self.number_of_features = images.shape[2] * images.shape[3]
print('number of features', self.number_of_features)
break

self.model = MultiLayerNetwork(number_of_features=self.number_of_features, number_of_outputs=10)
self.model = self.model.to(device=self.device)

self.optimizer = torch.optim.SGD(self.model.parameters(), lr=learning_rate)
self.loss_function = torch.nn.functional.cross_entropy

def run_batches(self, data_loader, batch_callback=None):
total_loss = 0
total_samples = 0
correct_predictions = 0
for _, (batch_x, batch_labels) in enumerate(data_loader):
# reshape from [100, 1, 28, 28] to [100, 28*28]
batch_x = batch_x.to(device=self.device)
batch_labels = batch_labels.to(device=self.device)
batch_samples = batch_x.shape[0]
total_samples += batch_samples
batch_x = batch_x.view(-1, self.number_of_features)
logics = self.model(batch_x)
batch_loss = self.loss_function(logics, batch_labels)
batch_predictions = torch.argmax(logics, dim=1)
batch_correct = batch_predictions == batch_labels
correct_predictions += batch_correct.sum()
if batch_callback is not None:
batch_callback(logics, batch_loss)
total_loss += batch_loss

average_loss = total_loss / total_samples
accuracy = float(correct_predictions) / total_samples

return average_loss.cpu(), accuracy

def batch_callback_train(self, _, batch_loss):
self.optimizer.zero_grad()
batch_loss.backward()
self.optimizer.step()

def train(self, number_of_epochs):
accuracy_train_per_epoch = []
accuracy_test_per_epoch = []
loss_train_per_epoch = []
loss_test_per_epoch = []
self.model.train(mode=True)
for epoch_index in range(number_of_epochs):
start_time = time.time()
self.run_batches(data_loader=self.loader_train, batch_callback=self.batch_callback_train)

with torch.no_grad():
epoch_loss_train, epoch_accuracy_train = self.run_batches(data_loader=self.loader_train)
epoch_loss_test, epoch_accuracy_test = self.run_batches(data_loader=self.loader_test)

loss_train_per_epoch.append(epoch_loss_train)
loss_test_per_epoch.append(epoch_loss_test)
accuracy_train_per_epoch.append(epoch_accuracy_train)
accuracy_test_per_epoch.append(epoch_accuracy_test)

passed_seconds= time.time() - start_time
print(f'epoch {epoch_index},'
f'process seconds {passed_seconds},'
f'loss train {epoch_loss_train},'
f'loss test {epoch_loss_test},'
f'accuracy train {epoch_accuracy_train},'
f'accuracy test {epoch_accuracy_test}')

self.model.train(mode=False)

plt.clf()
plt.plot(loss_train_per_epoch, color='b', label='train')
plt.plot(loss_test_per_epoch, color='r', label='test')
plt.legend()
plt.ylabel('Loss')
plt.xlabel('Epoch')
plt.savefig("loss.pdf")

plt.clf()
plt.plot(accuracy_train_per_epoch, color='b', label='train')
plt.plot(accuracy_test_per_epoch, color='r', label='test')
plt.legend()
plt.ylabel('Accuracy')
plt.xlabel('Epoch')
plt.savefig("accuracy.pdf")


def main():
random_seed = 42
number_of_epochs = 10

torch.manual_seed(random_seed)
trainer = Trainer()
trainer.train(number_of_epochs)


main()


The results are:


using device: cpu
train samples 60000
test samples 60000
single batch dimensions: torch.Size([100, 1, 28, 28])
single batch label dimensions: torch.Size([100])
number of features 784
epoch 0,loss train 0.0054548028856515884,loss test 0.005281297955662012,accuracy train 0.86655,accuracy test 0.8739
epoch 1,loss train 0.003863557009026408,loss test 0.0037198178470134735,accuracy train 0.8947833333333334,accuracy test 0.8988
epoch 2,loss train 0.0033619378227740526,loss test 0.0032330257818102837,accuracy train 0.9052166666666667,accuracy test 0.9099
epoch 3,loss train 0.003106057411059737,loss test 0.0030021234415471554,accuracy train 0.9111333333333334,accuracy test 0.9147
epoch 4,loss train 0.0029036011546850204,loss test 0.002816939726471901,accuracy train 0.9166333333333333,accuracy test 0.9199
epoch 5,loss train 0.002762931864708662,loss test 0.0026854947209358215,accuracy train 0.9205,accuracy test 0.9247
epoch 6,loss train 0.0026315872091799974,loss test 0.0025823484174907207,accuracy train 0.9245,accuracy test 0.9259
epoch 7,loss train 0.0025059168692678213,loss test 0.0024447101168334484,accuracy train 0.92775,accuracy test 0.9299
epoch 8,loss train 0.00239209970459342,loss test 0.0023570044431835413,accuracy train 0.9314,accuracy test 0.9323
epoch 9,loss train 0.0023001916706562042,loss test 0.0022823973558843136,accuracy train 0.93395,accuracy test 0.9349