update folder
Browse files- data_setup.py +137 -0
- engine.py +194 -0
- experiments.py +41 -0
- exploration.ipynb +0 -0
- model_builder.py +50 -0
- predict.py +49 -0
- train.py +66 -0
- utils.py +152 -0
data_setup.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
contains functionality for creating pytorch dataloaders for image classification data
|
| 3 |
+
"""
|
| 4 |
+
import os
|
| 5 |
+
import torch
|
| 6 |
+
from torchvision import datasets, transforms
|
| 7 |
+
from torch.utils.data import DataLoader
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
import pathlib
|
| 10 |
+
import requests
|
| 11 |
+
import zipfile
|
| 12 |
+
from typing import Tuple, Dict, List
|
| 13 |
+
from torch.utils.data import Dataset
|
| 14 |
+
from PIL import Image
|
| 15 |
+
|
| 16 |
+
NUM_WORKERS = os.cpu_count()
|
| 17 |
+
|
| 18 |
+
# create custom dataset
|
| 19 |
+
def find_classes(directory: str) -> Tuple[list[str], Dict[str, int]]:
|
| 20 |
+
"""
|
| 21 |
+
Finds the class folder names in a target directory
|
| 22 |
+
"""
|
| 23 |
+
# 1. get the class names by scanning the target directory
|
| 24 |
+
classes = sorted(entry.name for entry in os.scandir(directory) if entry.is_dir())
|
| 25 |
+
|
| 26 |
+
# 2. raise an error is class names couldn't be found
|
| 27 |
+
if not classes:
|
| 28 |
+
raise FileNotFoundError(f"couldn't find any classes in {directory}")
|
| 29 |
+
|
| 30 |
+
# 3. create a dictionary of index labels
|
| 31 |
+
class_to_idx = {class_name: i for i, class_name in enumerate(classes)}
|
| 32 |
+
return classes, class_to_idx
|
| 33 |
+
|
| 34 |
+
# 1. subclass torch.utils.data.Dataset
|
| 35 |
+
class ImageFolderCustom(Dataset):
|
| 36 |
+
# 2. initialize the constructor
|
| 37 |
+
def __init__(self, targ_dir: str, heads: list[str], transform=None, is_training: bool = True):
|
| 38 |
+
# 3. create several attributes
|
| 39 |
+
# get all the image paths
|
| 40 |
+
self.training = []
|
| 41 |
+
self.testing = []
|
| 42 |
+
for tag in heads:
|
| 43 |
+
self.img_list = list(Path(targ_dir / tag).glob("*.jpg"))
|
| 44 |
+
self.train_length = int(len(self.img_list) * 0.8)
|
| 45 |
+
self.training.extend(self.img_list[:self.train_length])
|
| 46 |
+
self.testing.extend(self.img_list[self.train_length:])
|
| 47 |
+
|
| 48 |
+
if is_training:
|
| 49 |
+
self.paths = self.training
|
| 50 |
+
else:
|
| 51 |
+
self.paths = self.testing
|
| 52 |
+
# setup transforms
|
| 53 |
+
self.transform = transform
|
| 54 |
+
# create classes and class_to_idx
|
| 55 |
+
self.classes, self.class_to_idx = find_classes(targ_dir)
|
| 56 |
+
|
| 57 |
+
# 4. create a function to load images
|
| 58 |
+
def load_image(self, index: int) -> Image.Image:
|
| 59 |
+
"opens an image via a path and returns it"
|
| 60 |
+
image_path = self.paths[index]
|
| 61 |
+
return Image.open(image_path)
|
| 62 |
+
|
| 63 |
+
# 5. overwrite __len__()
|
| 64 |
+
def __len__(self) -> int:
|
| 65 |
+
return len(self.paths)
|
| 66 |
+
|
| 67 |
+
# 6. overwrite __getitem__() to return a particular sample
|
| 68 |
+
def __getitem__(self, index: int) -> Tuple[torch.Tensor, int]:
|
| 69 |
+
"returns one sample of data, data and the label (X, y)"
|
| 70 |
+
img = self.load_image(index)
|
| 71 |
+
class_name = self.paths[index].parent.name # expects path in format: data_folder/class_name/image.jpg
|
| 72 |
+
class_idx = self.class_to_idx[class_name]
|
| 73 |
+
|
| 74 |
+
# transform if necessary
|
| 75 |
+
if self.transform:
|
| 76 |
+
return self.transform(img), class_idx
|
| 77 |
+
else:
|
| 78 |
+
return img, class_idx
|
| 79 |
+
|
| 80 |
+
def create_dataloaders(
|
| 81 |
+
image_dir: str,
|
| 82 |
+
heads: list[str],
|
| 83 |
+
train_transform: transforms.Compose,
|
| 84 |
+
test_transform: transforms.Compose,
|
| 85 |
+
batch_size: int,
|
| 86 |
+
num_workers: int=NUM_WORKERS
|
| 87 |
+
):
|
| 88 |
+
"""
|
| 89 |
+
creates training and testing DataLoaders.
|
| 90 |
+
|
| 91 |
+
Takes in a training directory and testing directory path and turns them
|
| 92 |
+
into pytorch datasets and then into pytorch dataloaders.
|
| 93 |
+
|
| 94 |
+
Args:
|
| 95 |
+
train_dir: path to training directory.
|
| 96 |
+
test_dir: path to testing directory
|
| 97 |
+
transform: torchvision transforms to perform on training and testing data.
|
| 98 |
+
batch_size: number of samples per batch in each of the dataloaders.
|
| 99 |
+
num_workers: an integer for number of workers per dataloader.
|
| 100 |
+
|
| 101 |
+
returns:
|
| 102 |
+
A tuple of (train_dataloader, test_dataloader, class_names).
|
| 103 |
+
where class_names is a list of the target classes.
|
| 104 |
+
|
| 105 |
+
Example usage:
|
| 106 |
+
train_dataloader, test_dataloader, class_names = create_dataloaders(train_dir=path/to/train_dir,
|
| 107 |
+
test_dir=path/to/test_dir,
|
| 108 |
+
transform=some_transform,
|
| 109 |
+
batch_size=32,
|
| 110 |
+
num_workers=4)
|
| 111 |
+
"""
|
| 112 |
+
|
| 113 |
+
# use ImageFolder to create datasets
|
| 114 |
+
train_data = ImageFolderCustom(targ_dir=image_dir, heads=heads, transform=train_transform, is_training=True)
|
| 115 |
+
|
| 116 |
+
test_data = ImageFolderCustom(targ_dir=image_dir, heads=heads, transform=test_transform, is_training=False)
|
| 117 |
+
|
| 118 |
+
# get class names
|
| 119 |
+
class_names = train_data.classes
|
| 120 |
+
|
| 121 |
+
# turn images into dataloaders
|
| 122 |
+
train_dataloader = DataLoader(
|
| 123 |
+
train_data,
|
| 124 |
+
batch_size=batch_size,
|
| 125 |
+
shuffle=True,
|
| 126 |
+
num_workers=num_workers,
|
| 127 |
+
pin_memory=True
|
| 128 |
+
)
|
| 129 |
+
test_dataloader = DataLoader(
|
| 130 |
+
test_data,
|
| 131 |
+
batch_size=batch_size,
|
| 132 |
+
shuffle=False,
|
| 133 |
+
num_workers=num_workers,
|
| 134 |
+
pin_memory=True
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
return train_dataloader, test_dataloader, class_names
|
engine.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
contains functions for training and testing a pytorch model
|
| 3 |
+
"""
|
| 4 |
+
import torch
|
| 5 |
+
|
| 6 |
+
from tqdm.auto import tqdm
|
| 7 |
+
from typing import Dict, List, Tuple
|
| 8 |
+
# from torch.utils.tensorboard.writer import SummaryWriter
|
| 9 |
+
|
| 10 |
+
def train_step(model: torch.nn.Module,
|
| 11 |
+
dataloader: torch.utils.data.DataLoader,
|
| 12 |
+
loss_fn: torch.nn.Module,
|
| 13 |
+
optimizer: torch.optim.Optimizer,
|
| 14 |
+
device: torch.device) -> Tuple[float, float]:
|
| 15 |
+
"""Trains a pytorch model for a single epoch
|
| 16 |
+
|
| 17 |
+
turns a target model to training mode then runs through all of the required training steps
|
| 18 |
+
(forward pass, loss calculation, optimizer step).
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
model: pytorch model
|
| 22 |
+
dataloader: dataloader insatnce for the model to be trained on
|
| 23 |
+
loss_fn: pytorch loss function to calculate loss
|
| 24 |
+
optimizer: pytorch optimizer to help minimize the loss function
|
| 25 |
+
device: target device
|
| 26 |
+
|
| 27 |
+
returns:
|
| 28 |
+
a tuple of training loss and training accuracy metrics
|
| 29 |
+
in the form (train_loss, train_accuracy)
|
| 30 |
+
"""
|
| 31 |
+
# put the model into training mode
|
| 32 |
+
model.train()
|
| 33 |
+
|
| 34 |
+
# setup train loss and train accuracy
|
| 35 |
+
train_loss, train_accuracy = 0, 0
|
| 36 |
+
|
| 37 |
+
# loop through data laoder batches
|
| 38 |
+
for batch, (X, y) in enumerate(dataloader):
|
| 39 |
+
# send data to target device
|
| 40 |
+
X, y = X.to(device), y.to(device)
|
| 41 |
+
|
| 42 |
+
# forward pass
|
| 43 |
+
logits = model(X)
|
| 44 |
+
|
| 45 |
+
# calculate loss and accumulate loss
|
| 46 |
+
loss = loss_fn(logits, y)
|
| 47 |
+
train_loss += loss
|
| 48 |
+
|
| 49 |
+
# optimizer zero grad
|
| 50 |
+
optimizer.zero_grad()
|
| 51 |
+
|
| 52 |
+
# loss backward
|
| 53 |
+
loss.backward()
|
| 54 |
+
|
| 55 |
+
# optimizer step
|
| 56 |
+
optimizer.step()
|
| 57 |
+
|
| 58 |
+
# calculate and accumulate accuracy metric across all batches
|
| 59 |
+
preds = torch.softmax(logits, dim=-1).argmax(dim=-1)
|
| 60 |
+
train_accuracy += (preds == y).sum().item()/len(preds)
|
| 61 |
+
|
| 62 |
+
# adjust metrics to get average loss and accuracy per batch
|
| 63 |
+
train_loss /= len(dataloader)
|
| 64 |
+
train_accuracy /= len(dataloader)
|
| 65 |
+
return train_loss, train_accuracy
|
| 66 |
+
|
| 67 |
+
def test_step(model: torch.nn.Module,
|
| 68 |
+
dataloader: torch.utils.data.DataLoader,
|
| 69 |
+
loss_fn: torch.nn.Module,
|
| 70 |
+
device: torch.device) -> Tuple[float, float]:
|
| 71 |
+
"""Tests a pytorch model for a single epoch
|
| 72 |
+
|
| 73 |
+
Turns a target model to eval mode and then performs a forward pass on a testing
|
| 74 |
+
dataset.
|
| 75 |
+
|
| 76 |
+
Args:
|
| 77 |
+
model: pytorch model
|
| 78 |
+
dataloader: dataloader insatnce for the model to be tested on
|
| 79 |
+
loss_fn: loss function to calculate loss (errors)
|
| 80 |
+
device: target device to compute on
|
| 81 |
+
|
| 82 |
+
returns:
|
| 83 |
+
A tuple of testing loss and testing accuracy metrics.
|
| 84 |
+
In the form (test_loss, test_accuracy)
|
| 85 |
+
"""
|
| 86 |
+
# put the model in eval mode
|
| 87 |
+
model.eval()
|
| 88 |
+
|
| 89 |
+
# setup test loss and test accuracy
|
| 90 |
+
test_loss, test_accuracy = 0, 0
|
| 91 |
+
|
| 92 |
+
# turn on inference mode
|
| 93 |
+
with torch.inference_mode():
|
| 94 |
+
# loop through all batches
|
| 95 |
+
for X, y in dataloader:
|
| 96 |
+
# send data to target device
|
| 97 |
+
X, y = X.to(device), y.to(device)
|
| 98 |
+
|
| 99 |
+
# forward pass
|
| 100 |
+
logits = model(X)
|
| 101 |
+
|
| 102 |
+
# calculate and accumulate loss
|
| 103 |
+
loss = loss_fn(logits, y)
|
| 104 |
+
test_loss += loss.item()
|
| 105 |
+
|
| 106 |
+
# calculate and accumulate accuracy
|
| 107 |
+
test_preds = torch.softmax(logits, dim=-1).argmax(dim=-1)
|
| 108 |
+
test_accuracy += ((test_preds == y).sum().item()/len(test_preds))
|
| 109 |
+
# adjust metrics to get average loss and accuracy per batch
|
| 110 |
+
test_loss /= len(dataloader)
|
| 111 |
+
test_accuracy /= len(dataloader)
|
| 112 |
+
return test_loss, test_accuracy
|
| 113 |
+
|
| 114 |
+
def train(model: torch.nn.Module,
|
| 115 |
+
train_dataloader: torch.utils.data.DataLoader,
|
| 116 |
+
test_dataloader: torch.utils.data.DataLoader,
|
| 117 |
+
optimizer: torch.optim.Optimizer,
|
| 118 |
+
loss_fn: torch.nn.Module,
|
| 119 |
+
epochs: int,
|
| 120 |
+
device: torch.device,
|
| 121 |
+
writer: torch.utils.tensorboard.writer.SummaryWriter) -> Dict[str, List]:
|
| 122 |
+
"""Trains and tests pytorch model
|
| 123 |
+
|
| 124 |
+
passes a target model through train_step() and test_step()
|
| 125 |
+
functions for a number of epochs, training and testing the model in the same epoch loop.
|
| 126 |
+
|
| 127 |
+
calculates, prints and stores evaluation metric throughout.
|
| 128 |
+
|
| 129 |
+
Args:
|
| 130 |
+
model: pytorch model
|
| 131 |
+
train_dataloader: DataLoader instance for the model to be trained on
|
| 132 |
+
test_dataloader: DataLoader instance for the model to be tested on
|
| 133 |
+
optimizer: pytorch optimizer
|
| 134 |
+
loss_fn: pytorch loss function
|
| 135 |
+
epochs: integer indicating how many epochs to train for
|
| 136 |
+
device: target device to compute on
|
| 137 |
+
|
| 138 |
+
returns:
|
| 139 |
+
A dictionaru of training and testing loss as well as training and testing accuracy
|
| 140 |
+
metrics. Each metric has a value in a list for each epoch.
|
| 141 |
+
|
| 142 |
+
In the form: {train_loss: [...],
|
| 143 |
+
train_acc: [...],
|
| 144 |
+
test_loss: [...],
|
| 145 |
+
test_acc: [...]}
|
| 146 |
+
"""
|
| 147 |
+
# create an empty dictionary
|
| 148 |
+
results = {
|
| 149 |
+
"train_loss": [],
|
| 150 |
+
"train_acc": [],
|
| 151 |
+
"test_loss": [],
|
| 152 |
+
"test_acc": []
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
# loop through training and testing steps for a number of epochs
|
| 156 |
+
for epoch in tqdm(range(epochs)):
|
| 157 |
+
train_loss, train_acc = train_step(model=model,
|
| 158 |
+
dataloader=train_dataloader,
|
| 159 |
+
loss_fn=loss_fn,
|
| 160 |
+
optimizer=optimizer,
|
| 161 |
+
device=device)
|
| 162 |
+
test_loss, test_acc = test_step(model=model,
|
| 163 |
+
dataloader=test_dataloader,
|
| 164 |
+
loss_fn=loss_fn,
|
| 165 |
+
device=device)
|
| 166 |
+
|
| 167 |
+
if epoch % 1 == 0:
|
| 168 |
+
print(
|
| 169 |
+
f"Epoch: {epoch+1} | "
|
| 170 |
+
f"train_loss: {train_loss:.4f} | "
|
| 171 |
+
f"train_acc: {train_acc:.4f} | "
|
| 172 |
+
f"test_loss: {test_loss:.4f} | "
|
| 173 |
+
f"test_acc: {test_acc:.4f}"
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
# update results dictionary
|
| 177 |
+
results["train_loss"].append(train_loss.item())
|
| 178 |
+
results["train_acc"].append(train_acc)
|
| 179 |
+
results["test_loss"].append(test_loss)
|
| 180 |
+
results["test_acc"].append(test_acc)
|
| 181 |
+
|
| 182 |
+
if writer:
|
| 183 |
+
# NEW: EXPERIMENT TRACKING
|
| 184 |
+
# add loss to SummaryWriter
|
| 185 |
+
writer.add_scalars(main_tag="Loss", tag_scalar_dict={"train loss": train_loss, "test loss": test_loss}, global_step=epoch)
|
| 186 |
+
# add accuracy to SummaryWriter
|
| 187 |
+
writer.add_scalars(main_tag="Accuracy", tag_scalar_dict={"train acc": train_acc, "test acc": test_acc}, global_step=epoch)
|
| 188 |
+
# track the pytorch model architecture
|
| 189 |
+
writer.add_graph(model=model, input_to_model=torch.randn(size=(32, 3, 224, 224)).to(device))
|
| 190 |
+
writer.close()
|
| 191 |
+
# END SummaryWriter tracking process
|
| 192 |
+
|
| 193 |
+
# return the filled results dictionaru
|
| 194 |
+
return results
|
experiments.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import utils
|
| 3 |
+
import model_builder as mb
|
| 4 |
+
import engine
|
| 5 |
+
|
| 6 |
+
# 3. loop through each dataloader
|
| 7 |
+
def run_experiment(train_dataloaders: dict, test_dataloader: torch.utils.data.DataLoader, num_epochs: int, models: list[str], class_names: list[str], device: torch.device = None):
|
| 8 |
+
# 1. set seed
|
| 9 |
+
utils.set_seeds(seed=42)
|
| 10 |
+
|
| 11 |
+
# 2. keep track of experiment numbers
|
| 12 |
+
experiment_number = 0
|
| 13 |
+
for dataloader_name, train_dataloader in train_dataloaders.items():
|
| 14 |
+
# 4. loop through each number of epochs
|
| 15 |
+
for epochs in num_epochs:
|
| 16 |
+
# 5. loop through each model name and create a new model based on the name
|
| 17 |
+
for model_name in models:
|
| 18 |
+
# 6. create information prints out
|
| 19 |
+
experiment_number += 1
|
| 20 |
+
print(f"[INFO] experiment number: {experiment_number}")
|
| 21 |
+
print(f"[INFO] model: {model_name}")
|
| 22 |
+
print(f"[INFO] dataloader: {dataloader_name}")
|
| 23 |
+
print(f"[INFO] number of epochs: {epochs}")
|
| 24 |
+
|
| 25 |
+
# 7. select the model
|
| 26 |
+
if model_name == "effnetb0":
|
| 27 |
+
model = mb.create_model_baseline_effnetb0(out_feats=len(class_names), device=device)
|
| 28 |
+
else:
|
| 29 |
+
model = mb.create_model_baseline_effnetb2(out_feats=len(class_names), device=device)
|
| 30 |
+
|
| 31 |
+
# 8. create a new loss function for every model
|
| 32 |
+
loss_fn = torch.nn.CrossEntropyLoss()
|
| 33 |
+
optimizer = torch.optim.Adam(params=model.parameters(), lr=0.001)
|
| 34 |
+
|
| 35 |
+
# 9. train target model with target dataloaders and track experiment
|
| 36 |
+
engine.train(model=model, train_dataloader=train_dataloader, test_dataloader=test_dataloader, optimizer=optimizer, loss_fn=loss_fn, epochs=epochs, device=device, writer=utils.create_writer(experiment_name=dataloader_name, model_name=model_name, extra=f"{epochs}_epochs"))
|
| 37 |
+
|
| 38 |
+
# 10. save the model to file
|
| 39 |
+
save_filepath = f"{model_name}_{dataloader_name}_{epochs}_epochs.pt"
|
| 40 |
+
utils.save_model(model=model, target_dir="models", model_name=save_filepath)
|
| 41 |
+
print("-"*50+"\n")
|
exploration.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
model_builder.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
contains pytorch model code to instantiate a TinyVGG model.
|
| 3 |
+
"""
|
| 4 |
+
import torch
|
| 5 |
+
from torch import nn
|
| 6 |
+
import torchvision
|
| 7 |
+
|
| 8 |
+
def create_model_baseline_effnetb0(out_feats: int, device: torch.device = None) -> torch.nn.Module:
|
| 9 |
+
weights = torchvision.models.EfficientNet_B0_Weights.DEFAULT
|
| 10 |
+
model = torchvision.models.efficientnet_b0(weights=weights).to(device)
|
| 11 |
+
|
| 12 |
+
for param in model.features.parameters():
|
| 13 |
+
param.requires_grad = False
|
| 14 |
+
|
| 15 |
+
torch.manual_seed(42)
|
| 16 |
+
torch.cuda.manual_seed(42)
|
| 17 |
+
|
| 18 |
+
# change the output layer
|
| 19 |
+
model.classifier = torch.nn.Sequential(
|
| 20 |
+
torch.nn.Dropout(p=0.2, inplace=True),
|
| 21 |
+
torch.nn.Linear(in_features=1280,
|
| 22 |
+
out_features=out_feats,
|
| 23 |
+
bias=True)).to(device)
|
| 24 |
+
|
| 25 |
+
model.name = "effnetb0"
|
| 26 |
+
print(f"[INFO] created a model {model.name}")
|
| 27 |
+
|
| 28 |
+
return model
|
| 29 |
+
|
| 30 |
+
def create_model_baseline_effnetb2(out_feats: int, device: torch.device = None) -> torch.nn.Module:
|
| 31 |
+
weights = torchvision.models.EfficientNet_B2_Weights.DEFAULT
|
| 32 |
+
model = torchvision.models.efficientnet_b2(weights=weights).to(device)
|
| 33 |
+
|
| 34 |
+
for param in model.features.parameters():
|
| 35 |
+
param.requires_grad = False
|
| 36 |
+
|
| 37 |
+
torch.manual_seed(42)
|
| 38 |
+
torch.cuda.manual_seed(42)
|
| 39 |
+
|
| 40 |
+
model.classifier = nn.Sequential(
|
| 41 |
+
nn.Dropout(p=0.3, inplace=True),
|
| 42 |
+
nn.Linear(in_features=1408,
|
| 43 |
+
out_features=out_feats,
|
| 44 |
+
bias=True)
|
| 45 |
+
).to(device)
|
| 46 |
+
|
| 47 |
+
model.name = "effnetb2"
|
| 48 |
+
print(f"[INFO] created a model {model.name}")
|
| 49 |
+
|
| 50 |
+
return model
|
predict.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import torch
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import requests
|
| 5 |
+
from PIL import Image
|
| 6 |
+
from torchvision import transforms
|
| 7 |
+
import data_setup, model_builder
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
parser = argparse.ArgumentParser()
|
| 12 |
+
parser.add_argument("-i", "--image", help="string of url to the image", type=str)
|
| 13 |
+
args = parser.parse_args()
|
| 14 |
+
|
| 15 |
+
URL = args.image # required
|
| 16 |
+
|
| 17 |
+
image_transform = transforms.Compose([
|
| 18 |
+
transforms.Resize(size=(224, 224)),
|
| 19 |
+
transforms.ToTensor(),
|
| 20 |
+
transforms.Normalize(mean=[0.485, 0.456, 0.406],
|
| 21 |
+
std=[0.229, 0.224, 0.225])])
|
| 22 |
+
|
| 23 |
+
IMAGE_PATH = Path("data") / "spoiled-fresh" / "FRUIT-16K"
|
| 24 |
+
|
| 25 |
+
classes = sorted(entry.name for entry in os.scandir(IMAGE_PATH) if entry.is_dir())
|
| 26 |
+
|
| 27 |
+
# load saved model
|
| 28 |
+
loaded_model = model_builder.create_model_baseline_effnetb2(out_feats=len(classes), device="cpu")
|
| 29 |
+
loaded_model.load_state_dict(torch.load("models/effnetb2_fruitsvegs0_5_epochs.pt", weights_only=True))
|
| 30 |
+
|
| 31 |
+
def pred_and_plot(model: torch.nn.Module,
|
| 32 |
+
image_path: str,
|
| 33 |
+
transform: transforms.Compose,
|
| 34 |
+
class_names: list[str] = None):
|
| 35 |
+
# load image
|
| 36 |
+
img = Image.open(requests.get(image_path, stream=True).raw).convert("RGB")
|
| 37 |
+
# setup transformed image
|
| 38 |
+
transformed_img = transform(img)
|
| 39 |
+
# forward pass
|
| 40 |
+
logits = model(transformed_img.unsqueeze(dim=0))
|
| 41 |
+
pred = torch.softmax(logits, dim=-1).argmax(dim=-1)
|
| 42 |
+
# plot the image along with the label
|
| 43 |
+
# plt.imshow(transformed_img.permute(1, 2, 0))
|
| 44 |
+
title = f"{class_names[pred]} | {torch.softmax(logits, dim=-1).max():.3f}"
|
| 45 |
+
plt.title(title)
|
| 46 |
+
print(title)
|
| 47 |
+
|
| 48 |
+
pred_and_plot(model=loaded_model, image_path=URL,
|
| 49 |
+
transform=image_transform, class_names=classes)
|
train.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import torch
|
| 3 |
+
import data_setup, engine, model_builder, utils
|
| 4 |
+
from torchvision import transforms, models
|
| 5 |
+
import argparse
|
| 6 |
+
|
| 7 |
+
parser = argparse.ArgumentParser()
|
| 8 |
+
parser.add_argument("-e", "--num_epochs", help="an integer to perform number of epochs", type=int)
|
| 9 |
+
parser.add_argument("-b", "--batch_size", help="an integer of number of element per batch", type=int)
|
| 10 |
+
# parser.add_argument("-hu", "--hidden_units", help="an integer of number of hidden units per layer", type=int)
|
| 11 |
+
parser.add_argument("-lr", "--learning_rate", help="a float for the learning rate", type=float)
|
| 12 |
+
|
| 13 |
+
args = parser.parse_args()
|
| 14 |
+
|
| 15 |
+
# setup hyperparameters
|
| 16 |
+
NUM_EPOCHS = args.num_epochs if args.num_epochs else 10
|
| 17 |
+
BATCH_SIZE = args.batch_size # required
|
| 18 |
+
# HIDDEN_UNITS = args.hidden_units if args.hidden_units else 10
|
| 19 |
+
LEARNING_RATE = args.learning_rate if args.learning_rate else 0.001
|
| 20 |
+
|
| 21 |
+
# setup directories
|
| 22 |
+
train_dir = "data/pizza_sushi_steak/train"
|
| 23 |
+
test_dir = "data/pizza_sushi_steak/test"
|
| 24 |
+
|
| 25 |
+
def main():
|
| 26 |
+
# setup device agnostic code
|
| 27 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 28 |
+
|
| 29 |
+
# create transforms
|
| 30 |
+
data_transform = transforms.Compose([
|
| 31 |
+
transforms.Resize(size=(224, 224)),
|
| 32 |
+
transforms.ToTensor(),
|
| 33 |
+
transforms.Normalize(mean=[0.485, 0.456, 0.406],
|
| 34 |
+
std=[0.229, 0.224, 0.225]),
|
| 35 |
+
])
|
| 36 |
+
|
| 37 |
+
# create DataLoaders with help from data_setup.py
|
| 38 |
+
train_dataloader, test_dataloader, class_names = data_setup.create_dataloaders(
|
| 39 |
+
train_dir=train_dir,
|
| 40 |
+
test_dir=test_dir,
|
| 41 |
+
transform=data_transform,
|
| 42 |
+
batch_size=BATCH_SIZE,
|
| 43 |
+
num_workers=0
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
# create model with help from model_builder.py
|
| 47 |
+
model = model_builder.create_model_baseline_effnetb0(out_feats=len(class_names), device=device)
|
| 48 |
+
|
| 49 |
+
# set loss and optimizer
|
| 50 |
+
loss_fn = torch.nn.CrossEntropyLoss()
|
| 51 |
+
optimizer = torch.optim.Adam(params=model.parameters(), lr=LEARNING_RATE)
|
| 52 |
+
|
| 53 |
+
# start training with help from engine.py
|
| 54 |
+
engine.train(model=model,
|
| 55 |
+
train_dataloader=train_dataloader,
|
| 56 |
+
test_dataloader=test_dataloader,
|
| 57 |
+
loss_fn=loss_fn,
|
| 58 |
+
optimizer=optimizer,
|
| 59 |
+
epochs=NUM_EPOCHS,
|
| 60 |
+
device=device)
|
| 61 |
+
|
| 62 |
+
# save the model with help from utils.py
|
| 63 |
+
utils.save_model(model=model, target_dir="models", model_name="tinyfood-effnet.pt")
|
| 64 |
+
|
| 65 |
+
if __name__ == '__main__':
|
| 66 |
+
main()
|
utils.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
contains various utility functions for pytorch model training and saving
|
| 3 |
+
"""
|
| 4 |
+
import torch
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import matplotlib.pyplot as plt
|
| 7 |
+
import torchvision
|
| 8 |
+
from PIL import Image
|
| 9 |
+
from torch.utils.tensorboard.writer import SummaryWriter
|
| 10 |
+
|
| 11 |
+
def save_model(model: torch.nn.Module,
|
| 12 |
+
target_dir: str,
|
| 13 |
+
model_name: str):
|
| 14 |
+
"""Saves a pytorch model to a target directory
|
| 15 |
+
|
| 16 |
+
Args:
|
| 17 |
+
model: target pytorch model
|
| 18 |
+
target_dir: string of target directory path to store the saved models
|
| 19 |
+
model_name: a filename for the saved model. Should be included either ".pth" or ".pt" as
|
| 20 |
+
the file extension.
|
| 21 |
+
"""
|
| 22 |
+
# create target directory
|
| 23 |
+
target_dir_path = Path(target_dir)
|
| 24 |
+
target_dir_path.mkdir(parents=True, exist_ok=True)
|
| 25 |
+
|
| 26 |
+
# create model save path
|
| 27 |
+
assert model_name.endswith(".pth") or model_name.endswith(".pt"), "model name should end with .pt or .pth"
|
| 28 |
+
model_save_path = target_dir_path / model_name
|
| 29 |
+
|
| 30 |
+
# save the model state_dict()
|
| 31 |
+
print(f"[INFO] Saving model to: {model_save_path}")
|
| 32 |
+
torch.save(obj=model.state_dict(), f=model_save_path)
|
| 33 |
+
|
| 34 |
+
def pred_and_plot_image(
|
| 35 |
+
model: torch.nn.Module,
|
| 36 |
+
image_path: str,
|
| 37 |
+
class_names: list[str] = None,
|
| 38 |
+
transform=None,
|
| 39 |
+
device: torch.device = "cuda" if torch.cuda.is_available() else "cpu",
|
| 40 |
+
):
|
| 41 |
+
"""Makes a prediction on a target image with a trained model and plots the image.
|
| 42 |
+
|
| 43 |
+
Args:
|
| 44 |
+
model (torch.nn.Module): trained PyTorch image classification model.
|
| 45 |
+
image_path (str): filepath to target image.
|
| 46 |
+
class_names (List[str], optional): different class names for target image. Defaults to None.
|
| 47 |
+
transform (_type_, optional): transform of target image. Defaults to None.
|
| 48 |
+
device (torch.device, optional): target device to compute on. Defaults to "cuda" if torch.cuda.is_available() else "cpu".
|
| 49 |
+
|
| 50 |
+
Returns:
|
| 51 |
+
Matplotlib plot of target image and model prediction as title.
|
| 52 |
+
|
| 53 |
+
Example usage:
|
| 54 |
+
pred_and_plot_image(model=model,
|
| 55 |
+
image="some_image.jpeg",
|
| 56 |
+
class_names=["class_1", "class_2", "class_3"],
|
| 57 |
+
transform=torchvision.transforms.ToTensor(),
|
| 58 |
+
device=device)
|
| 59 |
+
"""
|
| 60 |
+
|
| 61 |
+
# 1. Load in image and convert the tensor values to float32
|
| 62 |
+
img_list = Image.open(image_path)
|
| 63 |
+
|
| 64 |
+
# 2. Divide the image pixel values by 255 to get them between [0, 1]
|
| 65 |
+
# target_image = target_image / 255.0
|
| 66 |
+
|
| 67 |
+
# 3. Transform if necessary
|
| 68 |
+
if transform:
|
| 69 |
+
target_image = transform(img_list)
|
| 70 |
+
|
| 71 |
+
# 4. Make sure the model is on the target device
|
| 72 |
+
model.to(device)
|
| 73 |
+
|
| 74 |
+
# 5. Turn on model evaluation mode and inference mode
|
| 75 |
+
model.eval()
|
| 76 |
+
with torch.inference_mode():
|
| 77 |
+
# Add an extra dimension to the image
|
| 78 |
+
target_image = target_image.unsqueeze(dim=0)
|
| 79 |
+
|
| 80 |
+
# Make a prediction on image with an extra dimension and send it to the target device
|
| 81 |
+
target_image_pred = model(target_image.to(device))
|
| 82 |
+
|
| 83 |
+
# 6. Convert logits -> prediction probabilities (using torch.softmax() for multi-class classification)
|
| 84 |
+
target_image_pred_probs = torch.softmax(target_image_pred, dim=1)
|
| 85 |
+
|
| 86 |
+
# 7. Convert prediction probabilities -> prediction labels
|
| 87 |
+
target_image_pred_label = torch.argmax(target_image_pred_probs, dim=1)
|
| 88 |
+
|
| 89 |
+
# 8. Plot the image alongside the prediction and prediction probability
|
| 90 |
+
plt.imshow(
|
| 91 |
+
target_image.squeeze().permute(1, 2, 0)
|
| 92 |
+
) # make sure it's the right size for matplotlib
|
| 93 |
+
if class_names:
|
| 94 |
+
title = f"Pred: {class_names[target_image_pred_label.cpu()]} | Prob: {target_image_pred_probs.max().cpu():.3f}"
|
| 95 |
+
else:
|
| 96 |
+
title = f"Pred: {target_image_pred_label} | Prob: {target_image_pred_probs.max().cpu():.3f}"
|
| 97 |
+
plt.title(title)
|
| 98 |
+
plt.axis(False)
|
| 99 |
+
|
| 100 |
+
def set_seeds(seed: int=42):
|
| 101 |
+
"""Sets random sets for torch operations.
|
| 102 |
+
|
| 103 |
+
Args:
|
| 104 |
+
seed (int, optional): Random seed to set. Defaults to 42.
|
| 105 |
+
"""
|
| 106 |
+
# Set the seed for general torch operations
|
| 107 |
+
torch.manual_seed(seed)
|
| 108 |
+
# Set the seed for CUDA torch operations (ones that happen on the GPU)
|
| 109 |
+
torch.cuda.manual_seed(seed)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def create_writer(experiment_name: str, model_name: str, extra: str=None) -> torch.utils.tensorboard.writer.SummaryWriter(): # type: ignore
|
| 113 |
+
"""
|
| 114 |
+
creates a torch.utils.tensorboard.writer.SummaryWriter() instance saving to a
|
| 115 |
+
specific log_dir.
|
| 116 |
+
|
| 117 |
+
log_dir is a combination of runs/timestamp/experiment_name/model_name/extra.
|
| 118 |
+
|
| 119 |
+
where timestamp is the current date in YYYY-MM-DD format.
|
| 120 |
+
|
| 121 |
+
Args:
|
| 122 |
+
experiment_name (str): Name of experiment
|
| 123 |
+
model_name (str): model name
|
| 124 |
+
extra (str, optional): anything extra to add to the directory. Defaults is None
|
| 125 |
+
|
| 126 |
+
Returns:
|
| 127 |
+
torch.utils.tensorboard.writer.SummaryWriter(): Instance of a writer saving to log_dir
|
| 128 |
+
|
| 129 |
+
Examples usage:
|
| 130 |
+
this is gonna create writer saving to "runs/2022-06-04/data_10_percent/effnetb2/5_epochs"
|
| 131 |
+
|
| 132 |
+
writer = create_writer(experiment_name="data_10_percent", model_name="effnetb2", extra="5_epochs")
|
| 133 |
+
|
| 134 |
+
This is the same as:
|
| 135 |
+
writer = SummaryWriter(log_dir="runs/2022-06-04/data_10_percent/effnetb2/5_epochs")
|
| 136 |
+
"""
|
| 137 |
+
|
| 138 |
+
from datetime import datetime
|
| 139 |
+
import os
|
| 140 |
+
|
| 141 |
+
# get the timestamp
|
| 142 |
+
timestamp = datetime.now().strftime("%Y-%m-%d")
|
| 143 |
+
|
| 144 |
+
if extra:
|
| 145 |
+
# create log directory path
|
| 146 |
+
log_dir = os.path.join("runs", timestamp, experiment_name, model_name, extra)
|
| 147 |
+
else:
|
| 148 |
+
log_dir = os.path.join("runs", timestamp, experiment_name, model_name)
|
| 149 |
+
|
| 150 |
+
print(f"[INFO] Created SummaryWriter(), saving to: {log_dir}")
|
| 151 |
+
|
| 152 |
+
return SummaryWriter(log_dir=log_dir)
|