leafora commited on
Commit
1025b47
·
verified ·
1 Parent(s): 0ade1e4

update folder

Browse files
Files changed (8) hide show
  1. data_setup.py +137 -0
  2. engine.py +194 -0
  3. experiments.py +41 -0
  4. exploration.ipynb +0 -0
  5. model_builder.py +50 -0
  6. predict.py +49 -0
  7. train.py +66 -0
  8. 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)