Skip to content
This repository has been archived by the owner on Aug 7, 2023. It is now read-only.

Add CLI params support + MobileNetV2 model + small refactoring #9

Merged
merged 10 commits into from
Mar 8, 2021
31 changes: 0 additions & 31 deletions mask_detector/dataset.py

This file was deleted.

Empty file.
33 changes: 33 additions & 0 deletions mask_detector/datasets/masked_face_net.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
""" Dataset module
rpoltorak marked this conversation as resolved.
Show resolved Hide resolved
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also do not get the name. Why masked face net?

Copy link
Contributor Author

@rpoltorak rpoltorak Feb 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it's the name of our current dataset. MaskedFaceNet :) Any suggestions are more than welcome

import cv2
import torch
import numpy as np
import pandas as pd

from PIL import Image
from torch.utils.data.dataset import Dataset
from torchvision.transforms import CenterCrop, Compose, Normalize, Resize, ToPILImage, ToTensor


class MaskedFaceNetDataset(Dataset):
def __init__(self, csv_file, image_size):
self.data_frame = pd.read_csv(csv_file)

self.transform = Compose([
Resize(image_size), # for MobileNetV2 - set image size to 256
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are creating logic for creating data set for a specific solution (MobileNetV2) I would suggest to just create a separate solution to do this, rather than creating all for one class/function.

Copy link
Contributor Author

@rpoltorak rpoltorak Feb 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is indeed a good one. Do you have any idea how we could name that? We need to think about naming that kind of combinations of dataset and model. I can't come up with a name for a combination of MobileNetV2 and MaskedFaceNet :) Naming is indeed one of the hardest part of making software :P

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we shouldn't combine dataset with a particular model.

Rather than that we should keep a generic dataset prep here (connected only with the dataset we are using, parsing labels, etc).

Transforms specific for any model (or even generic ones for data augmentation) should be only applied here, not defined. It should be easy to do that if we pass all transforms as a function in arguments and apply it here.

Maybe even we should get a step further, merging both model-related transforms and data augmentation-related ones 🤔

Had not much free time last week, this one I'll try to come up with some nice file structure (after I'll finally decide on some collab tool).

If you want some inspiration you can check a template a friend of mine is doing (related to his thesis if I'm not wrong). Though I plan not to dive into it to be unbiased when preparing our template.

Also a quick observation - almost always when something is difficult to name it means we are doing something wrong 😅 If functions / modules are nicely separated then it shouldn't be as difficult (at least naming the modules / functions. Naming variables is and always be the major problem 😄 ).

CenterCrop(224),
ToTensor(),
Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])

def __getitem__(self, key):
row = self.data_frame.iloc[key]

mask = torch.tensor([row['mask']], dtype=torch.float)
image = Image.open(row['image'])

return self.transform(image), mask

def __len__(self):
return len(self.data_frame)
73 changes: 47 additions & 26 deletions mask_detector/mask_classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,86 +5,107 @@
from torch.utils.data import DataLoader, random_split
from torch.optim import Adam
from pytorch_lightning import LightningModule, Trainer, seed_everything
from pytorch_lightning.metrics import Recall
from pytorch_lightning.callbacks import ModelCheckpoint
from sklearn.metrics import accuracy_score

from model import BasicCNN
from dataset import MaskDataset
from utils import train_val_test_split
from models.basic_cnn import BasicCNN
from models.mobile_net_v2 import MobileNetV2
from datasets.masked_face_net import MaskedFaceNetDataset


class MaskClassifier(LightningModule):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, probably it would be convenient to prepare some basic, semi-generic class for our DataModule.
It should define all the common operations for the task we have (image-processing), as probably most of them will be the same between different models.

Then we should extend this base class and overwrite specific methods for each Model we have (choosing correct datasets, defining some transformations etc).

What do you think?

def __init__(self, net, learning_rate=1e-3):
def __init__(self, net, learning_rate=0.001):
super().__init__()
self.save_hyperparameters()
self.net = net
self.learning_rate = learning_rate
self.recall = Recall()

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

def training_step(self, batch, batch_idx):
x, y = batch
out = self.net(x)

loss = binary_cross_entropy(out, y)
recall = self.recall(out, y)

self.log('train_loss', loss, on_epoch=True)
self.log('train_loss', loss, on_step=False, on_epoch=True)
self.log('train_recall', recall, on_step=False, on_epoch=True)

return loss

def validation_step(self, batch, batch_idx):
x, y = batch
out = self.net(x)
loss = binary_cross_entropy(out, y)
recall = self.recall(out, y)

self.log('val_loss', loss, on_step=False, on_epoch=True)
self.log('val_recall', recall, on_step=False, on_epoch=True)

self.log('valid_loss', loss, on_step=True)
return loss

def test_step(self, batch, batch_idx):
x, y = batch
out = self.net(x)
loss = binary_cross_entropy(out, y)
recall = self.recall(out, y)

_, out = torch.max(out, dim=1)
val_acc = accuracy_score(out.cpu(), y.cpu())
val_acc = torch.tensor(val_acc)
self.log('test_loss', loss, on_step=False, on_epoch=True)
self.log('test_recall', recall, on_step=False, on_epoch=True)

return {'test_loss': loss, 'test_acc': val_acc}
return loss

def configure_optimizers(self):
# self.hparams available because we called self.save_hyperparameters()
return Adam(self.parameters(), lr=self.hparams.learning_rate)
return Adam(self.parameters(), lr=self.learning_rate)

@staticmethod
def add_model_specific_args(parent_parser):
parser = ArgumentParser(parents=[parent_parser], add_help=False)
parser.add_argument('--learning_rate', type=float, default=0.001)
return parser


def cli_main():
seed_everything(1234)

# ------------
# args
# ------------
parser = ArgumentParser()

parser.add_argument('--batch_size', default=32, type=int)
parser.add_argument('--image_size', default=120, type=int)

parser = Trainer.add_argparse_args(parser)
parser = MaskClassifier.add_model_specific_args(parser)
args = parser.parse_args()

# ------------
# data
# ------------
dataset = MaskDataset(csv_file='data/dataframe/mask_df.csv')
dataset = MaskedFaceNetDataset(
csv_file='data/dataframe/mask_df.csv', image_size=args.image_size)
ds_train, ds_validate, ds_test = train_val_test_split(
dataset, train_ratio=0.8, validate_ratio=0.1, test_ratio=0.1)

train_loader = DataLoader(ds_train, batch_size=128)
val_loader = DataLoader(ds_validate, batch_size=128)
test_loader = DataLoader(ds_test, batch_size=128)
train_loader = DataLoader(ds_train, batch_size=args.batch_size)
val_loader = DataLoader(ds_validate, batch_size=args.batch_size)
test_loader = DataLoader(ds_test, batch_size=args.batch_size)

# ------------
# model
# ------------
net = BasicCNN()
model = MaskClassifier(net, learning_rate=0.0001)
net = MobileNetV2()
model = MaskClassifier(net, learning_rate=args.learning_rate)

# ------------
# training
# ------------
checkpoint_callback = ModelCheckpoint(
verbose=True,
monitor='test_acc',
mode='max'
)
trainer = Trainer(max_epochs=1, checkpoint_callback=checkpoint_callback)
trainer.fit(model, train_loader, val_loader)
trainer = Trainer.from_argparse_args(args)
result = trainer.fit(model, train_loader, val_loader)

# ------------
# testing
Expand Down
42 changes: 0 additions & 42 deletions mask_detector/model.py

This file was deleted.

7 changes: 7 additions & 0 deletions mask_detector/models/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Models

## Current performance measures

| Model | Params | Loss | Recall |
| ----------- | ----------------------------------------------------- | ------ | ------ |
| MobileNetV2 | max_epochs=10<br>batch_size=32<br>learning_rate=0.001 | 0.1449 | 0.9794 |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having only loss and recall can be very misleading without having the precision as well.

Recall does not inform about any false positives. I suppose you have checked the dataset and if classes are properly balanced.

We have low loss value - though it does not mean anything. We do not know how to interpret this, or is loss 0.1 much better than 0.2. This is why usually additionally accuracy is presented.
On its own accuracy is very risky metric (without precision and recall data we could have a 99% accuracy when model is e.g. always outputting the same value, but classes were not balanced).

However, it describes what is the most important in evaluating the model outcome - how accurate the model is in the most intuitive way. If we see Recall / Precision / F1 score of e.g. 0.7 we need to think how to interpret it relating to the dataset, possible predictions etc. If we have an accuracy of 0.8 we already see that 80% of all predictions are correct. Then we can investigate other metrics to directly compare models, but that accuracy is the true goal anyways.

Also we miss precision. Here is a nice presentation what this could lead to (based on the situation from a shooting range):
image

I would opt for tracking all of these metrics (as they are not so slow to compute anyways). We never know when such information could become helpful.

Empty file.
43 changes: 43 additions & 0 deletions mask_detector/models/basic_cnn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from torch.nn import Dropout, Module, Conv2d, Flatten, Linear, MaxPool2d, ReLU, Sequential, Sigmoid, Softmax, Flatten


class BasicCNN(Module):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be nice to add some results for a tested classifier. Maybe in README? Without that kind of data, it's hard to say anything about a solution.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love that idea. Created separate README.md file in models directory. You could check this here: mask_detector/models/README.md

def __init__(self):
super().__init__()

self.convLayers1 = Sequential(
Conv2d(3, 32, kernel_size=(3, 3), padding=(1, 1)),
ReLU(),
MaxPool2d(kernel_size=(2, 2)),
Dropout(p=0.3)
)

self.convLayers2 = Sequential(
Conv2d(32, 64, kernel_size=(3, 3), padding=(1, 1)),
ReLU(),
MaxPool2d(kernel_size=(2, 2)),
Dropout(p=0.3)
)

self.convLayers3 = Sequential(
Conv2d(64, 128, kernel_size=(3, 3), padding=(1, 1)),
ReLU(),
MaxPool2d(kernel_size=(2, 2)),
Dropout(p=0.3)
)

self.linearLayers = Sequential(
Flatten(),
Linear(in_features=128*15*15, out_features=1024),
ReLU(),
Linear(in_features=1024, out_features=1),
Sigmoid()
)

def forward(self, output):
output = self.convLayers1(output)
output = self.convLayers2(output)
output = self.convLayers3(output)
output = self.linearLayers(output)
Comment on lines +38 to +41
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could try to investigate if this generates some calculation efficiency cost. There could be the case that PyTorch is optimizing all the calculations in 1 Sequential layer, that we lose keeping separate Sequentials. Maybe it would be better to store them as arrays and then convert to 1 Sequential?

TBC (To Be Checked) though, it may be not a problem at all.


return output
30 changes: 30 additions & 0 deletions mask_detector/models/mobile_net_v2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import torch
rpoltorak marked this conversation as resolved.
Show resolved Hide resolved
from torch.nn import AvgPool2d, Dropout, Flatten, Module, Conv2d, Linear, MaxPool2d, ReLU, Sequential, Sigmoid


class MobileNetV2(Module):
def __init__(self):
super().__init__()

net = torch.hub.load('pytorch/vision:v0.6.0',
'mobilenet_v2', pretrained=True)

self.features = net.features

self.classifier = Sequential(
AvgPool2d(kernel_size=(7, 7)),
Flatten(),
Linear(in_features=1280, out_features=128),
Dropout(p=0.5),
ReLU(),
Linear(in_features=128, out_features=1),
Sigmoid()
)

def forward(self, output):
with torch.no_grad():
output = self.features(output)

output = self.classifier(output)

return output