diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100755 index d79b107..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. In '...' directory, run command '...' -2. See error (copy&paste full log, including exceptions and **stacktraces**). - -Please copy&paste text instead of screenshots for better searchability. - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Desktop (please complete the following information):** - - OS: [e.g. Linux Ubuntu 20.04, Windows 10] - - PyTorch version (e.g., pytorch 1.7.1) - - CUDA toolkit version (e.g., CUDA 11.0) - - NVIDIA driver version - - GPU [e.g., Titan V, RTX 3090] - - Docker: did you use Docker? If yes, specify docker image URL (e.g., nvcr.io/nvidia/pytorch:20.12-py3) - -**Additional context** -Add any other context about the problem here. diff --git a/CNN_embeddings_projector/read_tsv.py b/CNN_embeddings_projector/read_tsv.py index 5e34b13..5d249fc 100644 --- a/CNN_embeddings_projector/read_tsv.py +++ b/CNN_embeddings_projector/read_tsv.py @@ -1,39 +1,45 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# File : read_tsv.py -# Modified : 01.02.2022 -# By : Sandra Carrasco +# Last Modified : 01.02.2022 +# By : Sandra Carrasco """ Compute cosine distance between embeddings from tsv file. """ -import pandas as pd import csv from scipy.spatial import distance import numpy as np import matplotlib.pyplot as plt +from argparse import ArgumentParser -metadata = list(csv.reader(open("/workspace/stylegan2-ada-pytorch/CNN_embeddings_projector/projections_vs_reals_nosprite/00000/default/metadata.tsv"), delimiter="\t")) -embeddings = list(csv.reader(open("/workspace/stylegan2-ada-pytorch/CNN_embeddings_projector/projections_vs_reals_nosprite/00000/default/tensors.tsv"), delimiter="\t")) - -#embeddings already ordered from x1, to x1, from x2, to x2 .... -distances = [] -for i in range(0,len(embeddings),2): - emb_from = list(map(float, embeddings[i])) - emb_to = list(map(float, embeddings[i+1])) - distances.append( distance.cosine(emb_from,emb_to) ) - -textfile = open("/workspace/stylegan2-ada-pytorch/CNN_embeddings_projector/projections_vs_reals_nosprite/distances.txt", "w") -for element in distances: - textfile.write(str(element) + "\n") -textfile.close() - -distances = np.array(distances) -Q1 = np.quantile(distances, 0.25) -Q2 = np.quantile(distances, 0.5) -Q3 = np.quantile(distances, 0.75) -his = plt.hist(distances) -distances_indeces_ordered = np.argsort(distances) -indeces_min_distance = distances_indeces_ordered[:2] # index = img name img0000idx.class.x.from.png +if __name__ == '__main__': + parser = ArgumentParser() + parser.add_argument("--metadata", type=str, + help='path to metadata file') + parser.add_argument("--embeddings_path", type=str, default=None, + help='path to embeddings saved as tensors.tsv') + parser.add_argument("--save_path", type=str, + help='path to save distances in text file') + args = parser.parse_args() + + metadata = csv.reader(open(args.metadata), delimiter="\t") + embeddings = list(csv.reader(open(args.embeddings_path), delimiter="\t")) + + # embeddings already ordered from x1, to x1, from x2, to x2 .... + distances = [] + for i in range(0, len(embeddings), 2): + emb_from = list(map(float, embeddings[i])) + emb_to = list(map(float, embeddings[i + 1])) + distances.append(distance.cosine(emb_from, emb_to)) + + textfile = open(args.save_path, "w") + for element in distances: + textfile.write(str(element) + "\n") + textfile.close() + + distances = np.array(distances) + Q1 = np.quantile(distances, 0.25) + Q2 = np.quantile(distances, 0.5) + Q3 = np.quantile(distances, 0.75) + his = plt.hist(distances) + distances_indeces_ordered = np.argsort(distances) \ No newline at end of file diff --git a/README.md b/README.md index 8f8edf6..10b7ebf 100755 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Datasets are stored as uncompressed ZIP archives containing uncompressed PNG fil Custom datasets can be created from a folder containing images; see [`python dataset_tool.py --help`](./docs/dataset-tool-help.txt) for more information. Alternatively, the folder can also be used directly as a dataset, without running it through `dataset_tool.py` first, but doing so may lead to suboptimal performance. -**ISIC 2020**: Download the [ISIC 2020 dataset](https://www.kaggle.com/c/siim-isic-melanoma-classification) and create ZIP archive: +**ISIC 2020**: Download the [ISIC 2020 dataset](https://www.kaggle.com/nroman/melanoma-external-malignant-256) and create ZIP archive: ```.bash python dataset_tool.py --source=/tmp/isic-dataset --dest=~/datasets/isic256x256.zip --width=256 --height=256 @@ -146,10 +146,35 @@ python generate.py --outdir=out --projected_w=out/projected_w.npz \ --class=1 --network=~/pretrained/conditionalGAN.pkl ``` +## Classification with EfficientNet-B2 + +In our studies generated synthetic images were used in binary classification task between melanoma and non-melanoma cases. To run training with Efficientnet-B2 use following command: + +```.bash +python melanoma_classifier.py --syn_data_path=~/generated/ \ + --real_data_path=~/melanoma-external-malignant-256/ \ + --synt_n_imgs="0,15" +``` + +In above example `--syn_data_path` argument indicates path for synthetic images, +`--real_data_path` - real images and `--synt_n_imgs` stands for n non-melanoma, k melanoma synthetic images (measured in kimg) to add to the real data. We reported our studis using wandb (use `--wandb_flag` argument to report accuracy and loss for your own experiments). `--only_reals` flag enable training only for real images, while `--only_syn` will allow to take all artificial images from directory with synthetic images. + +To make a diagnosis using trained model use [`predict.py`](predict.py) script. + + +## Visualizing the latent space +[`embeddings_projector.py`](https://github.com/aidotse/stylegan2-ada-pytorch/blob/main/embeddings_projector.py) performs the two following tasks: + +* Project embeddings of a CNN used as feature extractor. (`--use_cnn`) + +* Project w-vectors. + +This generates a `metadata.tsv`, `tensors.tsv` and (optionally using `--sprite` flag) a sprite of the images. These files can be uploaded in the [Tensorboard Projector](https://www.tensorflow.org/tensorboard/tensorboard_projector_plugin) , which graphically represent these embeddings. + ## Measuring authenticity -We additionaly calculated cosine distance between embeddings from tsv file. -For details see [read_tsv.py`](./CNN_embeddings_projector/read_tsv.py). +We additionaly calculated cosine distances between the CNN embeddings from the tsv file. +For details see [`read_tsv.py`](./CNN_embeddings_projector/read_tsv.py). ```.bash python ./CNN_embeddings_projector/read_tsv.py --metadata=metadata.tsv \ @@ -165,4 +190,4 @@ This work is made available under the [Nvidia Source Code License](https://nvlab ## Acknowledgements -The project was developed during the first rotation of the [Eye for AI Program](https://www.ai.se/en/eyeforai) at the AI Competence Center of [Sahlgrenska University Hospital](https://www.sahlgrenska.se/en/). Eye for AI initiative is a global program focused on bringing more international talents into the Swedish AI landscape. +The project was developed during the first rotation of the [Eye for AI Program](https://www.ai.se/en/eyeforai) at the AI Competence Center of [Sahlgrenska University Hospital](https://www.sahlgrenska.se/en/). Eye for AI initiative is a global program focused on bringing more international talents into the Swedish AI landscape. \ No newline at end of file diff --git a/create_dataset_json.py b/create_dataset_json.py deleted file mode 100644 index 45ca3da..0000000 --- a/create_dataset_json.py +++ /dev/null @@ -1,31 +0,0 @@ -import json -from pathlib import Path -import pandas as pd -from sklearn.model_selection import train_test_split -from pathlib import Path -import os - -""" -CSV_DIR = Path('/home/Data/melanoma_external_256/') -df = pd.read_csv(CSV_DIR/'train_concat.csv') -train_split, valid_split = train_test_split (df, stratify=df.target, test_size = 0.20, random_state=42) -train_df=pd.DataFrame(train_split) - -labels_list = [] -for n in range(len(train_df)): - labels_list.append([train_df.iloc[n].image_name,int(train_df.iloc[n].target)]) -""" - - -labels_list = [] -input_images = [str(f) for f in sorted(Path('/workspace/melanoma_isic_dataset/all_melanoma/SAM_Dataset').rglob('*')) if os.path.isfile(f)] -for img_path in input_images: - label = img_path.split('/')[-2] - if label == 'In_situ': - labels_list.append([img_path, '0']) - else: - labels_list.append([img_path, '1']) - -labels_list_dict = { "labels" : labels_list} -with open("/workspace/melanoma_isic_dataset/all_melanoma/SAM_Dataset/labels.json", "w") as outfile: - json.dump(labels_list_dict, outfile) diff --git a/docs/stylegan2-ada-teaser-1024x252.png b/docs/stylegan2-ada-teaser-1024x252.png deleted file mode 100755 index 14eb641..0000000 Binary files a/docs/stylegan2-ada-teaser-1024x252.png and /dev/null differ diff --git a/embeddings_projector.py b/embeddings_projector.py index 8969fe4..16892fc 100644 --- a/embeddings_projector.py +++ b/embeddings_projector.py @@ -1,27 +1,18 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# File : embeddings_projector.py -# Modified : 22.01.2022 -# By : Sandra Carrasco +# Last Modified : 22.01.2022 +# By : Sandra Carrasco -import numpy as np import os import PIL.Image as Image -from matplotlib import pylab as P -import cv2 from torchvision import transforms from torch.utils.tensorboard import SummaryWriter import torch -from argparse import ArgumentParser -import json +from argparse import ArgumentParser +import sys from tqdm import tqdm from efficientnet_pytorch import EfficientNet -from utils import Net +from utils import Net, testing_transforms from pathlib import Path -import random -# from torchsummary import summary -import saliency.core as saliency def select_n_random(data, labels, n=100): ''' @@ -32,129 +23,123 @@ def select_n_random(data, labels, n=100): perm = torch.randperm(len(data)) return data[perm][:n], labels[perm][:n] -parser = ArgumentParser() -parser.add_argument("--use_cnn", action='store_true', help='retrieve features from the last layer of EfficientNet B2') -parser.add_argument("--sprite", action='store_true') -args = parser.parse_args() - - -# Setting up GPU for processing or CPU if GPU isn't available -device = torch.device("cuda" if torch.cuda.is_available() else "cpu") - -transform = transforms.ToTensor() -testing_transforms = transforms.Compose([transforms.Resize(256), - transforms.CenterCrop(256), - transforms.ToTensor(), - transforms.Normalize([0.485, 0.456, 0.406], - [0.229, 0.224, 0.225])]) - -if args.use_cnn: - directories = ["/workspace/stylegan2-ada-pytorch/projector/00000"] - filename = "dataset.json" - - arch = EfficientNet.from_pretrained('efficientnet-b2') - model = Net(arch=arch, return_feats=True) - # summary(model, (3, 256, 256), device='cpu') - model.load_state_dict(torch.load('/workspace/stylegan2-ada-pytorch/CNN_trainings/melanoma_model_0_0.9672_16_12_onlysyn.pth')) - - model.eval() - model.to(device) - images_pil = [] - metadata_f = [] - embeddings = [] - """ - for directory in directories: - with open(os.path.join(directory, filename)) as file: - data = json.load(file)['labels'] - random.shuffle(data) - with torch.no_grad(): - for i, (img, label) in tqdm(enumerate(data)): - img_dir = os.path.join(directory,img) - img_net = torch.tensor(testing_transforms(Image.open(img_dir)).unsqueeze(0), dtype=torch.float32).to(device) - emb = model(img_net) - embeddings.append(emb.cpu()) - metadata_f.append(['4', img] if directory.split('/')[-1] == "processed_dataset_256_SAM" - else [label, img]) # to discern between SAM data and the rest - if args.sprite: - img_pil = transform(Image.open(img_dir).resize((100, 100))) - images_pil.append(img_pil) - - if i > 3200: - # ISIC 37k images, project only 6k random imgs - break - """ - # Repeat the process for randomly generated data - images = [str(f) for f in sorted(Path("/workspace/stylegan2-ada-pytorch/projector/00000").glob('*png')) if os.path.isfile(f)] - #labels = [2 if f.split('.jpg')[0][-1] == '0' else 3 for f in images] - labels = [] - for f in images: - if "from" in f: - labels.append( f.split('.from.png')[0][-1] ) - else: - labels.append( str( int(f.split('.to.png')[0][-1])+2 ) ) - - - - with torch.no_grad(): - for img_dir, label in tqdm(zip(images, labels)): - img_net = torch.tensor(testing_transforms(Image.open(img_dir)).unsqueeze(0), dtype=torch.float32).to(device) - emb = model(img_net) - embeddings.append(emb.cpu()) - metadata_f.append([label, img_dir.split('/')[-1]]) - if args.sprite: - img_pil = transform(Image.open(img_dir).resize((100, 100))) - images_pil.append(img_pil) - - embeddings_tensor = torch.stack(embeddings).squeeze() - if args.sprite: - images_pil = torch.stack(images_pil) - # default `log_dir` is "runs" - we'll be more specific here - writer = SummaryWriter('/workspace/stylegan2-ada-pytorch/CNN_embeddings_projector/projections_vs_reals_nosprite') - -else: - # This part can be used with G_mapping embeddings (vector w) - projections in the latent space - directory = "/workspace/stylegan2-ada-pytorch/projector/" - emb_f = "allvectorsf.txt" - metadata_f = "alllabelsf.txt" + +if __name__ == '__main__': + parser = ArgumentParser() + parser.add_argument("--use_cnn", + help=f'retrieve features from' + f'the last layer of EfficientNet B2', + action='store_true') + parser.add_argument("--sprite", action='store_true') + parser.add_argument("--model_path", type=str, + help='path to trained classifier EfficientNet-B2') + parser.add_argument("--projections_path", type=str, + help='path to generated projections') + parser.add_argument("--embeddings_path", type=str, default=None, + help='path to save embeddings') + args = parser.parse_args() + + # Setting up GPU for processing or CPU if GPU isn't available + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + transform = transforms.ToTensor() - with open(os.path.join(directory, emb_f)) as f: - embeddings = f.readlines() #[::2] - embeddings_tensor = torch.tensor( [float(i) for emb_line in embeddings for i in emb_line[:-2].split(' ') ] ).reshape(len(embeddings),-1) - - - with open(os.path.join(directory, metadata_f)) as f: - metadata=f.readlines() #[::2] - metadata_f = [[name.split('.')[0].split(' ')[0], name.split('.')[0].split(' ')[1]] for name in metadata] - - images_pil = torch.empty(len(metadata), 3, 100,100) - labels = [] - for i, line in enumerate(metadata): - label = int(line.split(' ')[0]) - if label == 0 or label==1: - img_name = '00000/'+ line.split(' ')[1].split('txt')[0]+ 'from.png' - elif label == 4: - img_name = 'SAM_data/'+ line.split(' ')[1].split('txt')[0]+ 'from.png' - else: - label_name = '0' if label == 2 else '1' - img_name = 'generated-20kpkl/'+ line.split(' ')[1].split('.')[0] + '_' + label_name + '.jpg' - #img_name = line.split(' ')[1].split('txt')[0] + 'from.png' # 0 img00000552.class.0.txt - - img_dir = os.path.join(directory,img_name) - img = transform(Image.open(img_dir).resize((100, 100))) - images_pil[i] = img - labels.append(label) - - # default `log_dir` is "runs" - we'll be more specific here - writer = SummaryWriter('/workspace/stylegan2-ada-pytorch/projector' + directory.split('/')[-1]) #('/home/stylegan2-ada-pytorch/projector') #('/workspace/melanoma_isic_dataset/stylegan2-ada-pytorch/projector') - -if args.sprite: - writer.add_embedding(embeddings_tensor, - metadata=metadata_f, - metadata_header=["label","image_name"], - label_img=images_pil) -else: - writer.add_embedding(embeddings_tensor, - metadata=metadata_f, - metadata_header=["label","image_name"]) -writer.close() \ No newline at end of file + if args.use_cnn: + if args.embeddings_path is None: + sys.exit("You mast provide embeddings path!") + + arch = EfficientNet.from_pretrained('efficientnet-b2') + model = Net(arch=arch, return_feats=True) + model.load_state_dict(torch.load(args.model_path)) + + model.eval() + model.to(device) + images_pil = [] + metadata_f = [] + embeddings = [] + + # Repeat the process for randomly generated data + images = [ + str(f) for f in sorted( + Path(args.projections_path).glob('*png') + ) if os.path.isfile(f)] + labels = [] + for f in images: + if "from" in f: + labels.append(f.split('.from.png')[0][-1]) + else: + labels.append(str(int(f.split('.to.png')[0][-1]) + 2)) + + with torch.no_grad(): + for img_dir, label in tqdm(zip(images, labels)): + img_net = torch.tensor(testing_transforms( + Image.open(img_dir)).unsqueeze(0), + dtype=torch.float32).to(device) + emb = model(img_net) + embeddings.append(emb.cpu()) + metadata_f.append([label, img_dir.split('/')[-1]]) + if args.sprite: + img_pil = transform(Image.open(img_dir).resize((100, 100))) + images_pil.append(img_pil) + + embeddings_tensor = torch.stack(embeddings).squeeze() + if args.sprite: + images_pil = torch.stack(images_pil) + # default `log_dir` is "runs" - we'll be more specific here + writer = SummaryWriter(args.embeddings_path) + else: + # This part can be used with G_mapping embeddings (vector w) + # - projections in the latent space + directory = args.projections_path + emb_f = "allvectorsf.txt" + metadata_f = "alllabelsf.txt" + transform = transforms.ToTensor() + + with open(os.path.join(directory, emb_f)) as f: + embeddings = f.readlines() # [::2] + embeddings_tensor = torch.tensor( + [float(i) for emb_line in embeddings for i in emb_line[ + :-2].split(' ')] + ).reshape(len(embeddings), -1) + + with open(os.path.join(directory, metadata_f)) as f: + metadata = f.readlines() # [::2] + metadata_f = [ + [ + name.split('.')[0].split( + ' ')[0], name.split('.')[0].split(' ')[1] + ] for name in metadata + ] + + images_pil = torch.empty(len(metadata), 3, 100, 100) + labels = [] + for i, line in enumerate(metadata): + label = int(line.split(' ')[0]) + if label == 0 or label == 1: + img_name = '00000/' + img_name += line.split(' ')[1].split('txt')[0] + 'from.png' + else: + label_name = '0' if label == 2 else '1' + img_name = 'generated-20kpkl/' + img_name += line.split(' ')[1].split('.')[0] + img_name += '_' + label_name + '.jpg' + + img_dir = os.path.join(directory, img_name) + img = transform(Image.open(img_dir).resize((100, 100))) + images_pil[i] = img + labels.append(label) + + # default `log_dir` is "runs" - we'll be more specific here + writer = SummaryWriter( + args.projections_path + directory.split('/')[-1]) + + if args.sprite: + writer.add_embedding(embeddings_tensor, + metadata=metadata_f, + metadata_header=["label", "image_name"], + label_img=images_pil) + else: + writer.add_embedding(embeddings_tensor, + metadata=metadata_f, + metadata_header=["label", "image_name"]) + writer.close() diff --git a/melanoma_classifier.py b/melanoma_classifier.py index c586685..0dffd13 100644 --- a/melanoma_classifier.py +++ b/melanoma_classifier.py @@ -1,33 +1,27 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# File : melanoma_cnn_efficientnet.py -# Modified : 12.01.2022 -# By : Sandra Carrasco +# Last Modified : 12.01.2022 +# By : Sandra Carrasco import numpy as np -import pandas as pd -import gc -from pathlib import Path -from argparse import ArgumentParser +import pandas as pd +import gc +from argparse import ArgumentParser import torch from torch import nn -from torch import optim +from torch import optim from torch.optim.lr_scheduler import ReduceLROnPlateau -from torch.utils.tensorboard import SummaryWriter - -from torch.utils.data import Dataset, DataLoader, Subset -# from imblearn.under_sampling import RandomUnderSampler - +from torch.utils.tensorboard import SummaryWriter import time -import datetime - -from sklearn.model_selection import StratifiedKFold, GroupKFold, train_test_split -from sklearn.metrics import accuracy_score, roc_auc_score, confusion_matrix, f1_score - +import datetime +from sklearn.metrics import accuracy_score, roc_auc_score, f1_score -import os +import os -from utils import * +from utils import (seed_everything, confussion_matrix, + add_pr_curve_tensorboard, + CustomDataset, plot_classes_preds, + seed_worker, load_model, testing_transforms, + training_transforms, load_synthetic_data, + load_isic_data) import wandb import warnings @@ -40,156 +34,161 @@ # Setting up GPU for processing or CPU if GPU isn't available device = torch.device("cuda" if torch.cuda.is_available() else "cpu") -print (device) +print(device) - -writer_path=f'training_classifiers_events/test_all_melanoma/{datetime.datetime.now().month}_{datetime.datetime.now().day}/' +writer_path = f'training_classifiers_events/test_all_melanoma/{datetime.datetime.now().month}_{datetime.datetime.now().day}/' writer = SummaryWriter(writer_path) - -### TRAINING ### -def train(model, train_loader, validate_loader, k_fold = 0, epochs = 10, es_patience = 3): +# TRAINING +def train(model, train_loader, validate_loader, + epochs=10, es_patience=3, wandb_flag=False): # Training model print('Starts training...') best_val = 0 criterion = nn.BCEWithLogitsLoss() # Optimizer (gradient descent): - optimizer = optim.Adam(model.parameters(), lr=0.0005) + optimizer = optim.Adam(model.parameters(), lr=0.0005) # Scheduler - scheduler = ReduceLROnPlateau(optimizer=optimizer, mode='max', patience=1, verbose=True, factor=0.2) - - loss_history=[] - train_acc_history=[] - val_loss_history=[] - val_auc_history=[] - val_f1_history=[] - + scheduler = ReduceLROnPlateau( + optimizer=optimizer, mode='max', + patience=es_patience, verbose=True, factor=0.2) + + loss_history = [] + train_acc_history = [] + val_loss_history = [] + val_auc_history = [] + val_f1_history = [] + patience = es_patience - Total_start_time = time.time() + Total_start_time = time.time() model.to(device) for e in range(epochs): - start_time = time.time() correct = 0 running_loss = 0 model.train() - + for i, (images, labels) in enumerate(train_loader): - + images, labels = images.to(device), labels.to(device) - + optimizer.zero_grad() - - output = model(images) - loss = criterion(output, labels.view(-1,1)) + + output = model(images) + loss = criterion(output, labels.view(-1, 1)) loss.backward() optimizer.step() - + # Training loss running_loss += loss.item() # Number of correct training predictions and training accuracy train_preds = torch.round(torch.sigmoid(output)) - - correct += (train_preds.cpu() == labels.cpu().unsqueeze(1)).sum().item() - - if i % 500 == 1: # == N every N minibatches - wandb.log({f'train/training_loss': loss, 'epoch':e}) - """ - # Log in Tensorboard - writer.add_figure('predictions vs. actuals', - plot_classes_preds(model, images, labels.type(torch.int)), - global_step=e+1) - """ - train_acc = correct / len(training_dataset) - val_loss, val_auc_score, val_accuracy, val_f1 = val(model, validate_loader, criterion) - - - training_time = str(datetime.timedelta(seconds=time.time() - start_time))[:7] - - print("Epoch: {}/{}.. ".format(e+1, epochs), - "Training Loss: {:.3f}.. ".format(running_loss/len(train_loader)), - "Training Accuracy: {:.3f}..".format(train_acc), - "Validation Loss: {:.3f}.. ".format(val_loss/len(validate_loader)), - "Validation Accuracy: {:.3f}".format(val_accuracy), - "Validation AUC Score: {:.3f}".format(val_auc_score), - "Validation F1 Score: {:.3f}".format(val_f1), - "Training Time: {}".format( training_time)) - - wandb.log({'train/Training acc': train_acc, 'epoch':e, 'val/Validation Acc': val_accuracy, - 'val/Validation Auc': val_auc_score, 'val/Validation Loss': val_loss/len(validate_loader)}) - - """ - # Log in Tensorboard - writer.add_scalar('training loss', running_loss/len(train_loader), e+1 ) - writer.add_scalar('Training acc', train_acc, e+1 ) - writer.add_scalar('Validation AUC Score', val_auc_score, e+1 ) - """ + correct += ( + train_preds.cpu() == labels.cpu().unsqueeze(1) + ).sum().item() + + if i % 500 == 1: # == N every N minibatches + if wandb_flag: + wandb.log({'train/training_loss': loss, 'epoch': e}) + else: + # Log in Tensorboard + writer.add_figure( + 'predictions vs. actuals', + plot_classes_preds( + model, images, labels.type(torch.int)), + global_step=e+1) + train_acc = correct / len(training_dataset) + val_loss, val_auc_score, val_accuracy, val_f1 = val( + model, validate_loader, criterion) + training_time = str( + datetime.timedelta(seconds=time.time() - start_time))[:7] + + print( + "Epoch: {}/{}.. ".format(e+1, epochs), + "Training Loss: {:.3f}.. ".format(running_loss/len(train_loader)), + "Training Accuracy: {:.3f}..".format(train_acc), + "Validation Loss: {:.3f}.. ".format(val_loss/len(validate_loader)), + "Validation Accuracy: {:.3f}".format(val_accuracy), + "Validation AUC Score: {:.3f}".format(val_auc_score), + "Validation F1 Score: {:.3f}".format(val_f1), + "Training Time: {}".format(training_time)) + if wandb_flag: + wandb.log( + {'train/Training acc': train_acc, 'epoch': e, + 'val/Validation Acc': val_accuracy, + 'val/Validation Auc': val_auc_score, + 'val/Validation Loss': val_loss/len(validate_loader)}) + else: + # Log in Tensorboard + writer.add_scalar( + 'training loss', running_loss/len(train_loader), e+1) + writer.add_scalar( + 'Training acc', train_acc, e+1) + writer.add_scalar( + 'Validation AUC Score', val_auc_score, e+1) scheduler.step(val_accuracy) - + if val_accuracy > best_val: - best_val = val_accuracy - wandb.run.summary["best_auc_score"] = val_auc_score - wandb.run.summary["best_acc_score"] = val_accuracy - patience = es_patience # Resetting patience since we have new best validation accuracy - model_path = os.path.join(writer_path, f'./classifier_{args.model}_{best_val:.4f}_{datetime.datetime.now()}.pth') - torch.save(model.state_dict(), model_path) # Saving current best model + best_val = val_accuracy + if wandb_flag: + wandb.run.summary["best_auc_score"] = val_auc_score + wandb.run.summary["best_acc_score"] = val_accuracy + # Resetting patience since we have new best validation accuracy + patience = es_patience + model_path = os.path.join( + writer_path, + f'./classifier_{args.model}_{best_val:.4f}' + f'_{datetime.datetime.now()}.pth') + # Saving current best model + torch.save(model.state_dict(), model_path) print(f'Saving model in {model_path}') else: patience -= 1 if patience == 0: print('Early stopping. Best Val f1: {:.3f}'.format(best_val)) break - - loss_history.append(running_loss) - train_acc_history.append(train_acc) - val_loss_history.append(val_loss) - #val_acc_history.append(val_accuracy) + loss_history.append(running_loss) + train_acc_history.append(train_acc) + val_loss_history.append(val_loss) val_auc_history.append(val_auc_score) val_f1_history.append(val_f1) - #val_loss_r_history.append(val_loss_r) - #val_auc_r_history.append(val_auc_score_r) - - total_training_time = str(datetime.timedelta(seconds=time.time() - Total_start_time ))[:7] + total_training_time = str( + datetime.timedelta(seconds=time.time() - Total_start_time))[:7] print("Total Training Time: {}".format(total_training_time)) - del train_loader, validate_loader, images gc.collect() - return model_path #, val_loss_r_history, val_auc_r_history + return model_path - -def val(model, validate_loader, criterion): + +def val(model, validate_loader, criterion): model.eval() - preds=[] - all_labels=[] + preds = [] + all_labels = [] # Turning off gradients for validation, saves memory and computations with torch.no_grad(): - val_loss = 0 - val_correct = 0 - for val_images, val_labels in validate_loader: - - val_images, val_labels = val_images.to(device), val_labels.to(device) - + val_images = val_images.to(device) + val_labels = val_labels.to(device) val_output = model(val_images) - val_loss += (criterion(val_output, val_labels.view(-1,1))).item() + val_loss += (criterion(val_output, val_labels.view(-1, 1))).item() val_pred = torch.sigmoid(val_output) - preds.append(val_pred.cpu()) all_labels.append(val_labels.cpu()) - pred=np.vstack(preds).ravel() + + pred = np.vstack(preds).ravel() pred2 = torch.tensor(pred) val_gt = np.concatenate(all_labels) val_gt2 = torch.tensor(val_gt) - + val_accuracy = accuracy_score(val_gt2, torch.round(pred2)) val_auc_score = roc_auc_score(val_gt, pred) val_f1_score = f1_score(val_gt, np.round(pred)) @@ -198,159 +197,159 @@ def val(model, validate_loader, criterion): def test(model, test_loader): - test_preds=[] - all_labels=[] - misclassified = [] - low_confidence = [] + test_preds = [] + all_labels = [] with torch.no_grad(): - + for _, (test_images, test_labels) in enumerate(test_loader): - - test_images, test_labels = test_images.to(device), test_labels.to(device) - + test_images = test_images.to(device) + test_labels = test_labels.to(device) + test_output = model(test_images) test_pred = torch.sigmoid(test_output) - + test_preds.append(test_pred.cpu()) all_labels.append(test_labels.cpu()) - - test_pred=np.vstack(test_preds).ravel() + + test_pred = np.vstack(test_preds).ravel() test_pred2 = torch.tensor(test_pred) test_gt = np.concatenate(all_labels) test_gt2 = torch.tensor(test_gt) + ''' + # For edge cases indeces_misclassified = np.where(test_gt != np.round(test_pred))[0] - well_classified = list(set(list(range(0, len(test_gt2)))) - set(indeces_misclassified.tolist())) - edge_cases = np.where( (test_gt[well_classified] - test_pred[well_classified]) > 0.25 )[0] - + well_classified = list( + set(list(range( + 0, len(test_gt2)))) - set(indeces_misclassified.tolist())) + edge_cases = np.where( + (test_gt[well_classified] - test_pred[well_classified] + ) > 0.25)[0] + ''' + try: - test_accuracy = accuracy_score(test_gt2.cpu(), torch.round(test_pred2)) + test_accuracy = accuracy_score( + test_gt2.cpu(), + torch.round(test_pred2)) test_auc_score = roc_auc_score(test_gt, test_pred) test_f1_score = f1_score(test_gt, np.round(test_pred)) except: test_auc_score = 0 test_f1_score = 0 pass - + # plot all the pr curves for i in range(len(classes)): add_pr_curve_tensorboard(i, test_pred2, test_gt2, writer) - print("Test Accuracy: {:.5f}, ROC_AUC_score: {:.5f}, F1 score: {:.4f}".format(test_accuracy, test_auc_score, test_f1_score)) + print( + "Test Accuracy:{:.5f}, ROC_AUC_score: {:.5f}, F1 score: {:.4f}".format( + test_accuracy, test_auc_score, test_f1_score) + ) return test_pred, test_gt, test_accuracy if __name__ == "__main__": parser = ArgumentParser() - parser.add_argument("--syn_data_path", type=str, default='/workspace/generated-no-valset') - parser.add_argument("--real_data_path", type=str, default='/workspace/melanoma_isic_dataset') - parser.add_argument("--model", type=str, default='efficientnet-b2', choices=["efficientnet-b2", "googlenet", "resnet50"]) + parser.add_argument( + "--syn_data_path", type=str, default='/workspace/generated-no-valset') + parser.add_argument( + "--real_data_path", type=str, + default='/workspace/melanoma_isic_dataset') + parser.add_argument( + "--model", type=str, default='efficientnet-b2', + choices=["efficientnet-b2", "googlenet", "resnet50"]) parser.add_argument("--epochs", type=int, default='30') - parser.add_argument("--es", type=int, default='4', help = "Iterations for Early Stopping") - parser.add_argument("--kfold", type=int, default='3', help='number of folds for stratisfied kfold') - parser.add_argument("--unbalanced", action='store_true', help='train with 15% melanoma') - parser.add_argument("--only_reals", action='store_true', help='train using only real images') - parser.add_argument("--only_syn", action='store_true', help='train using only synthetic images') + parser.add_argument( + "--es", type=int, default='3', help="Iterations for Early Stopping") + parser.add_argument( + "--unbalanced", action='store_true', help='train with 15% melanoma') + parser.add_argument( + "--only_reals", action='store_true', + help='train using only real images') + parser.add_argument( + "--only_syn", action='store_true', + help='train using only synthetic images') parser.add_argument("--tags", type=str, default='whole isic') - parser.add_argument("--synt_n_imgs", type=str, default="0,15", help='n benign, n melanoma K synthetic images to add to the real data') + parser.add_argument( + "--synt_n_imgs", type=str, default="0,15", + help='n benign, n melanoma K synthetic images to add to the real data') + parser.add_argument( + "--wandb_flag", + action="store_true", + default=False, + help="Launch experiment and log metrics with wandb", + ) args = parser.parse_args() - wandb.init(project="dai-healthcare" , entity='eyeforai', group='isic', tags=[args.tags], config={"model": args.model}) - wandb.config.update(args) - - # under_sampler = RandomUnderSampler(random_state=42) - # train_df_res, _ = under_sampler.fit_resample(train_df, train_df.target) - - """ - train_0, test_0 = train_test_split(ind_0, test_size=0.2, random_state=3) - train_1, test_1 = train_test_split(ind_1, test_size=0.2, random_state=3) - train_id = np.append(train_0,train_1) - test_ix = np.append(test_0,test_1) - train_0, val_0 = train_test_split(ind_0, test_size=0.25, random_state=3) # 0.25 x 0.8 = 0.2 - train_1, val_1 = train_test_split(ind_1, test_size=0.25, random_state=3) # 0.25 x 0.8 = 0.2 - train_id = np.append(train_0,train_1) - val_id = np.append(val_0, val_1) - """ - + if args.wandb_flag: + wandb.init(project="dai-healthcare", entity='eyeforai', group='isic', + tags=[args.tags], config={"model": args.model}) + wandb.config.update(args) + isic_train_df, validation_df = load_isic_data(args.real_data_path) - synt_train_df = load_synthetic_data(args.syn_data_path, args.synt_n_imgs, args.only_syn) + synt_train_df = load_synthetic_data( + args.syn_data_path, args.synt_n_imgs, args.only_syn) if args.only_syn: train_df = synt_train_df elif args.only_reals: train_df = isic_train_df - else: - train_df = pd.concat([isic_train_df, synt_train_df]) - - """ - fold=0 - skf = StratifiedKFold(n_splits=args.kfold) - for fold, (train_ix, val_ix) in enumerate(skf.split(train_img, train_gt)): - print(len(train_ix), len(val_ix)) - train_df = df.iloc[train_ix].reset_index(drop=True) - validation_df = df.iloc[val_ix].reset_index(drop=True) - - Loading the datasets with the transforms previously defined - train_id, val_id, test_id = create_split(args.data_path, unbalanced=args.unbalanced) - """ - - # training_dataset = Synth_Dataset(source_dir = args.syn_data_path, transform = training_transforms, id_list = None, unbalanced=args.unbalanced) # CustomDataset(df = train_df_res, img_dir = train_img_dir, train = True, transforms = training_transforms ) - # train_id, val_id = create_split(args.syn_data_path, unbalanced=args.unbalanced) - training_dataset = CustomDataset(df = train_df, train = True, transforms = training_transforms ) - #Synth_Dataset(source_dir = args.syn_data_path, transform = training_transforms, id_list = train_id, unbalanced=args.unbalanced) # CustomDataset(df = train_df_res, img_dir = train_img_dir, train = True, transforms = training_transforms ) - validation_dataset = CustomDataset(df = validation_df, train = True, transforms = training_transforms) - - #testing_dataset = Synth_Dataset(source_dir = args.data_path, transform = testing_transforms, id_list = range(len(test_gt)), input_img=test_img) - testing_dataset = CustomDataset(df = validation_df, train = True, transforms = testing_transforms ) - - - train_loader = torch.utils.data.DataLoader(training_dataset, batch_size=32, num_workers=4, worker_init_fn=seed_worker, shuffle=True) - validate_loader = torch.utils.data.DataLoader(validation_dataset, batch_size=16, num_workers=4, worker_init_fn=seed_worker, shuffle = False) - # validate_loader_real = torch.utils.data.DataLoader(validation_dataset, batch_size=16, num_workers=4, worker_init_fn=seed_worker, shuffle = False) - test_loader = torch.utils.data.DataLoader(testing_dataset, batch_size=16, num_workers=4, worker_init_fn=seed_worker, shuffle = False) + else: + train_df = pd.concat([isic_train_df, synt_train_df]) + + training_dataset = CustomDataset( + df=train_df, train=True, transforms=training_transforms) + validation_dataset = CustomDataset( + df=validation_df, train=True, + transforms=training_transforms) + testing_dataset = CustomDataset( + df=validation_df, train=True, transforms=testing_transforms) + + train_loader = torch.utils.data.DataLoader( + training_dataset, batch_size=32, num_workers=4, + worker_init_fn=seed_worker, shuffle=True) + validate_loader = torch.utils.data.DataLoader( + validation_dataset, batch_size=16, num_workers=4, + worker_init_fn=seed_worker, shuffle=False) + test_loader = torch.utils.data.DataLoader( + testing_dataset, batch_size=16, num_workers=4, + worker_init_fn=seed_worker, shuffle=False) print(len(training_dataset), len(validation_dataset)) - print(len(train_loader),len(validate_loader),len(test_loader)) - - """ - # Visualizing some example images in Tensorboard - dataiter = iter(train_loader) - imgs, labels =dataiter.next() - imgs_list = [renormalize(img) for img in imgs] - img_grid = utils.make_grid(imgs_list) - writer.add_image('train_loader_images', img_grid) - - # Visualize the model graph in Tensorboard - writer.add_graph(model, imgs.to(device)) - """ + print(len(train_loader), len(validate_loader), len(test_loader)) # Load model model = load_model(args.model) print(f'Model {args.model} loaded.') - # If we need to freeze the pretrained model parameters to avoid backpropogating through them, turn to "False" + # If we need to freeze the pretrained model parameters + # to avoid backpropogating through them, turn to "False" for parameter in model.parameters(): parameter.requires_grad = True - #Total Parameters (If the model is unfrozen the trainning params will be the same as the Total params) + # Total Parameters + # (If the model is unfrozen the trainning params + # will be the same as the Total params) total_params = sum(p.numel() for p in model.parameters()) print(f'{total_params:,} total parameters.') total_trainable_params = sum( p.numel() for p in model.parameters() if p.requires_grad) print(f'{total_trainable_params:,} training parameters.') - model_path = train(model, train_loader, validate_loader, epochs=args.epochs, es_patience=args.es) + model_path = train(model, train_loader, + validate_loader, + epochs=args.epochs, + es_patience=args.es, + wandb_flag=args.wandb_flag) - del training_dataset, validation_dataset + del training_dataset, validation_dataset gc.collect() - - ### TESTING THE NETWORK ### - #model_path = '/home/stylegan2-ada-pytorch/models_trained_with_synth/melanoma_model_unbal_0.9999899287601407.pth' + # TESTING THE NETWORK model.load_state_dict(torch.load(model_path)) model.eval() model.to(device) - test_pred, test_gt, test_accuracy = test(model, test_loader) + test_pred, test_gt, test_accuracy = test(model, test_loader) - ### CONFUSSION MATRIX ### + # CONFUSSION MATRIX confussion_matrix(test_gt, test_pred, test_accuracy, writer_path) - \ No newline at end of file diff --git a/predict.py b/predict.py index cea65d9..f3cb72a 100644 --- a/predict.py +++ b/predict.py @@ -1,38 +1,27 @@ + #!/usr/bin/env python3 # -*- coding: utf-8 -*- # File : predict.py -# Modified : 17.02.2022 -# By : Sandra Carrasco - -import numpy as np +# Modified : 08.03.2022 +# By : Sandra Carrasco import re import os from typing import List -import matplotlib.pyplot as plt -from pathlib import Path -from PIL import Image +import matplotlib.pyplot as plt import torch -#import torchtoolbox.transform as transforms -from torchvision import transforms -from efficientnet_pytorch import EfficientNet -import seaborn as sb -from argparse import ArgumentParser +from argparse import ArgumentParser from melanoma_classifier import test -from utils import load_model, load_isic_data, load_synthetic_data, CustomDataset , confussion_matrix -import pandas as pd -from sklearn.model_selection import train_test_split -from datetime import date, datetime +from utils import (load_model, load_isic_data, predict, + process_image, imshow, + load_synthetic_data, CustomDataset, + confussion_matrix, testing_transforms) -testing_transforms = transforms.Compose([transforms.Resize(256), - transforms.CenterCrop(256), - transforms.ToTensor(), - transforms.Normalize([0.485, 0.456, 0.406], - [0.229, 0.224, 0.225])]) - def num_range(s: str) -> List[int]: - '''Accept either a comma separated list of numbers 'a,b,c' or a range 'a-c' and return as a list of ints.''' - + ''' + Accept either a comma separated list of numbers + 'a,b,c' or a range 'a-c' and return as a list of ints. + ''' range_re = re.compile(r'^(\d+)-(\d+)$') m = range_re.match(s) if m: @@ -40,125 +29,44 @@ def num_range(s: str) -> List[int]: vals = s.split(',') return [int(x) for x in vals] -def process_image(image_path): - ''' Scales, crops, and normalizes a PIL image for a PyTorch model, - returns an Numpy array - ''' - # Process a PIL image for use in a PyTorch model - - pil_image = Image.open(image_path) - - # Resize - if pil_image.size[0] > pil_image.size[1]: - pil_image.thumbnail((5000, 256)) - else: - pil_image.thumbnail((256, 5000)) - - # Crop - left_margin = (pil_image.width-256)/2 - bottom_margin = (pil_image.height-256)/2 - right_margin = left_margin + 256 - top_margin = bottom_margin + 256 - - pil_image = pil_image.crop((left_margin, bottom_margin, right_margin, top_margin)) - - # Normalize - np_image = np.array(pil_image)/255 - mean = np.array([0.485, 0.456, 0.406]) - std = np.array([0.229, 0.224, 0.225]) - np_image = (np_image - mean) / std - - # PyTorch expects the color channel to be the first dimension but it's the third dimension in the PIL image and Numpy array - # Color channel needs to be first; retain the order of the other two dimensions. - np_image = np_image.transpose((2, 0, 1)) - - return np_image - -def imshow(image, ax=None, title=None): - if ax is None: - fig, ax = plt.subplots() - - # PyTorch tensors assume the color channel is the first dimension - # but matplotlib assumes is the third dimension - image = image.transpose((1, 2, 0)) - - # Undo preprocessing - mean = np.array([0.485, 0.456, 0.406]) - std = np.array([0.229, 0.224, 0.225]) - image = std * image + mean - - if title is not None: - ax.set_title(title) - - # Image needs to be clipped between 0 and 1 or it looks like noise when displayed - image = np.clip(image, 0, 1) - - ax.imshow(image) - - return ax - -def predict(image_path, model, topk=1): #just 2 classes from 1 single output - ''' Predict the class (or classes) of an image using a trained deep learning model. - ''' - #image = process_image(image_path) - - # Convert image to PyTorch tensor first - #image = torch.from_numpy(image).type(torch.cuda.FloatTensor) - #print(image.shape) - #print(type(image)) - - # Returns a new tensor with a dimension of size one inserted at the specified position. - #image = image.unsqueeze(0) - - output = model(testing_transforms(Image.open(image_path)).type(torch.cuda.FloatTensor).unsqueeze(0)) # same output - - probabilities = torch.sigmoid(output) - - # Probabilities and the indices of those probabilities corresponding to the classes - top_probabilities, top_indices = probabilities.topk(topk) - - # Convert to lists - top_probabilities = top_probabilities.detach().type(torch.FloatTensor).numpy().tolist()[0] - top_indices = top_indices.detach().type(torch.FloatTensor).numpy().tolist()[0] - - top_classes = [] - - if probabilities > 0.5 : - top_classes.append("Melanoma") - else: - top_classes.append("Benign") - - return top_probabilities, top_classes - -def plot_diagnosis(predict_image_path, model,label): +def plot_diagnosis(predict_image_path, model, label): img_nb = predict_image_path.split('/')[-1].split('.')[0] - probs, classes = predict(predict_image_path, model) - print(probs) - print(classes) + probs, classes = predict(predict_image_path, model) # Display an image along with the diagnosis of melanoma or benign # Plot Skin image input image - plt.figure(figsize = (6,10)) - plot_1 = plt.subplot(2,1,1) + plt.figure(figsize=(6, 10)) + plot_1 = plt.subplot(2, 1, 1) image = process_image(predict_image_path) imshow(image, plot_1) - font = {"color": 'g'} if 'Benign' in classes and label == 0 or 'Melanoma' in classes and label == 1 else {"color": 'r'} - plot_1.set_title(f"Diagnosis: {classes}, Output (prob) {probs[0]:.4f}, Label: {label}", fontdict=font); + if (('Benign' in classes and label == 0) + or ('Melanoma' in classes and label == 1)): + font = {"color": 'g'} + else: + font = {"color": 'r'} + plot_1.set_title( + f"Diagnosis: {classes}, Output (prob) {probs[0]:.4f}, Label: {label}", + fontdict=font) plt.savefig(f'{args.out_path}/prediction_{img_nb}.png') - - if __name__ == "__main__": - parser = ArgumentParser() - parser.add_argument('--seeds', type=num_range, help='List of random seeds Ex. 0-3 or 0,1,2') - parser.add_argument("--data_path", type=str, default='/workspace/generated-no-valset') - parser.add_argument("--model_path", type=str, default='/workspace/stylegan2-ada-pytorch/CNN_trainings/melanoma_model_0_0.9225_16_12_train_reals+15melanoma.pth') - parser.add_argument("--out_path", type=str, default='', help='output path for confussion matrix') - + parser = ArgumentParser() + parser.add_argument('--seeds', type=num_range, + help='List of random seeds Ex. 0-3 or 0,1,2') + parser.add_argument("--data_path", type=str) + parser.add_argument("--model_path", type=str) + parser.add_argument("--out_path", type=str, default='', + help='output path for confussion matrix') + parser.add_argument( + "--plot", + action="store_true", + default=False, + help="Plot and save image with diagnosis", + ) args = parser.parse_args() # Setting up GPU for processing or CPU if GPU isn't available @@ -169,26 +77,26 @@ def plot_diagnosis(predict_image_path, model,label): model.load_state_dict(torch.load(args.model_path)) model.eval() - - if "SAM" in args.data_path: - input_images = [str(f) for f in sorted(Path(args.data_path).rglob('*jpg')) if os.path.isfile(f)] - y = [1 for i in range(len(input_images))] - test_df = pd.DataFrame({'image_name': input_images, 'target': y}) - elif "isic" in args.data_path: + if "isic" in args.data_path: # For testing with ISIC dataset _, test_df = load_isic_data(args.data_path) - else: + else: test_df = load_synthetic_data(args.data_path, "3,3") - - testing_dataset = CustomDataset(df = test_df, train = True, transforms = testing_transforms ) - test_loader = torch.utils.data.DataLoader(testing_dataset, batch_size=16, shuffle = False) - test_pred, test_gt, test_accuracy = test(model, test_loader) + testing_dataset = CustomDataset(df=test_df, train=True, + transforms=testing_transforms) + test_loader = torch.utils.data.DataLoader(testing_dataset, + batch_size=16, + shuffle=False) + test_pred, test_gt, test_accuracy = test(model, test_loader) confussion_matrix(test_gt, test_pred, test_accuracy, args.out_path) - # Plot diagnosis - """ for seed_idx, seed in enumerate(args.seeds): - print('Predicting image for seed %d (%d/%d) ...' % (seed, seed_idx, len(args.seeds))) - path = '/home/Data/generated/seed' + str(seed).zfill(4) - path += '_0.png' if seed <= 5000 else '_1.png' - plot_diagnosis(path, model) """ \ No newline at end of file + # Plot diagnosis + if args.plot: + for seed_idx, seed in enumerate(args.seeds): + print( + f'Predicting image for seed ' + f'{seed} ({seed_idx}/{len(args.seeds)}) ...') + path = os.path.join(args.out_path, 'seed' + str(seed).zfill(4)) + path += '_0.png' if seed <= 5000 else '_1.png' + plot_diagnosis(path, model) diff --git a/run_projector_generator.py b/run_projector_generator.py index 14b0def..4e1db11 100644 --- a/run_projector_generator.py +++ b/run_projector_generator.py @@ -1,63 +1,67 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# File : server_mp.py -# Modified : 22.01.2022 -# By : Sandra Carrasco +# last modified : 22.01.2022 +# By : Sandra Carrasco import numpy as np import os from tqdm import tqdm import random import json -from argparse import ArgumentParser - -random.seed(0) -os.environ['PYTHONHASHSEED'] = str(0) -np.random.seed(0) - -parser = ArgumentParser() -parser.add_argument("--filename", type=str, default='dataset.json') -parser.add_argument("--directory", type=str, default='/workspace/stylegan2-ada-pytorch/processed_dataset_256_SAM') -parser.add_argument('--initial_tqdm', type=int, help='Restart projection', default=0) - -parser.add_argument("--task", type=str, default='project', help='Choose task project/generate', - choices=['generate', 'project']) - -parser.add_argument('--network', help='Network pickle filename', default=None) -parser.add_argument('--trunc', type=float, help='Truncation psi', default=1) -parser.add_argument('--class_idx',type=int, help='Class label (unconditional if not specified)') -parser.add_argument('--num_imgs',type=int) -parser.add_argument('--outdir', help='Where to save the output images', type=str, default=None) - -args = parser.parse_args() - -filename = args.filename -directory = args.directory - -if args.task == 'project': - with open(os.path.join(directory, filename)) as file: - data = json.load(file)['labels'] - - for img, label in tqdm(data, initial=args.initial_tqdm): - img_dir = os.path.join(directory,img) - label = 1 # FOR PROJECTING SAM DATA img[1] - - execute = "python projector.py " - execute = execute + " --outdir=" + args.outdir - execute = execute + " --target=" + img_dir - execute = execute + " --network=/workspace/stylegan2-ada-pytorch/training_runs/network-snapshot-020000.pkl" - execute = execute + " --class_label " + str(label) - execute = execute + " --num-steps 1000" - - #print(execute) - os.system(execute) - #exit(-1) -else: - execute = "python generate.py " - execute = execute + " --outdir=" + args.outdir - execute = execute + " --trunc=" + str(args.trunc) - execute = execute + " --network=" + args.network - execute = execute + " --class=" + str(args.class_idx) - execute = execute + " --seeds=" + str(np.random.randint(0,1000000,args.num_imgs)).replace('[','').replace(']','').replace(' ',',') - - os.system(execute) +from argparse import ArgumentParser + +if __name__ == "__main__": + random.seed(0) + os.environ['PYTHONHASHSEED'] = str(0) + np.random.seed(0) + + parser = ArgumentParser() + parser.add_argument("--filename", type=str, default='dataset.json') + parser.add_argument("--directory", type=str, + help='path to directory with images resided to 256x256' + ) + parser.add_argument('--initial_tqdm', type=int, help='Restart projection', + default=0) + parser.add_argument('--num_images', type=int, + help='Number of images to project', default=1000000) + parser.add_argument("--task", type=str, default='project', + help='Choose task project/generate', + choices=['generate', 'project']) + parser.add_argument('--network', help='Network pickle filename', + default=None) + parser.add_argument('--trunc', type=float, help='Truncation psi', + default=1) + parser.add_argument('--class_idx', type=int, + help='Class label (unconditional if not specified)') + parser.add_argument('--num_imgs', type=int) + parser.add_argument('--outdir', help='Where to save the output images', + type=str, default=None) + args = parser.parse_args() + + filename = args.filename + directory = args.directory + + if args.task == 'project': + with open(os.path.join(directory, filename)) as file: + data = json.load(file)['labels'] + + for img, label in tqdm(data, initial=args.initial_tqdm): + img_dir = os.path.join(directory, img) + + execute = "python projector.py " + execute = execute + " --outdir=" + args.outdir + execute = execute + " --target=" + img_dir + execute = execute + " --network=" + args.network + execute = execute + " --class_label " + str(label) + execute = execute + " --num-steps 1000" + + os.system(execute) + else: + execute = "python generate.py " + execute = execute + " --outdir=" + args.outdir + execute = execute + " --trunc=" + str(args.trunc) + execute = execute + " --network=" + args.network + execute = execute + " --class=" + str(args.class_idx) + execute = execute + " --seeds=" + str( + np.random.randint(0, args.num_images, args.num_imgs)).replace( + '[', '').replace(']', '').replace(' ', ',') + + os.system(execute) diff --git a/siim_isic_pylightning_seresnext50.py b/siim_isic_pylightning_seresnext50.py deleted file mode 100755 index 56dcef6..0000000 --- a/siim_isic_pylightning_seresnext50.py +++ /dev/null @@ -1,422 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# File : server_mp.py -# Modified : 22.01.2022 -# By : Sandra Carrasco - -import os -import random -import argparse -from pathlib import Path -import PIL.Image as Image -import pandas as pd -import numpy as np -from argparse import ArgumentParser, Namespace -from sklearn.metrics import roc_auc_score -from sklearn.model_selection import train_test_split - -import pytorch_lightning as pl -import torch.utils.data as tdata -import torch -import torch.nn as nn -import torchvision.models as models -from torchsummary import summary -from torchvision import transforms - -import torch.nn.functional as F - -from efficientnet_pytorch import EfficientNet -import json - -# Reproductibility -SEED = 33 -random.seed(SEED) -os.environ['PYTHONHASHSEED'] = str(SEED) -np.random.seed(SEED) -torch.manual_seed(SEED) -torch.cuda.manual_seed(SEED) -torch.backends.cudnn.deterministic = True -torch.backends.cudnn.benchmark = False - - - -def dict_to_args(d): - - args = argparse.Namespace() - - def dict_to_args_recursive(args, d, prefix=''): - for k, v in d.items(): - if type(v) == dict: - dict_to_args_recursive(args, v, prefix=k) - elif type(v) in [tuple, list]: - continue - else: - if prefix: - args.__setattr__(prefix + '_' + k, v) - else: - args.__setattr__(k, v) - - dict_to_args_recursive(args, d) - return args - - -class SIIMDataset(tdata.Dataset): - - def __init__(self, df, transform, test=False): - self.df = df - self.transform = transform - self.test = test - - def __len__(self): - return len(self.df) - - def __getitem__(self, idx): - meta = self.df.iloc[idx] - #image_fn = meta['image_name'] + '.jpg' # Use this when training with original images - image_fn = meta['image_name'] + '.jpg' - if self.test: - img = Image.open(str(IMAGE_DIR / ('test/test/' + image_fn))) - else: - img = Image.open(str(IMAGE_DIR / ('train_224/' + image_fn))) - - if self.transform is not None: - img = self.transform(img) - - if self.test: - return {'image': img} - else: - return {'image': img, 'target': meta['target']} - - - -class Synth_Dataset(tdata.Dataset): - - def __init__(self, transform, test=True): - self.transform = transform - self.test = test - self.input_images = [str(f) for f in sorted(Path(source_dir).rglob('*')) if os.path.isfile(f)] - - def __len__(self): - return len(self.input_images) - - def __getitem__(self, idx): - #class 0 - bening , class_1 - malign - image_fn = self.input_images[idx] #f'{idx:04d}_{idx%2}' - - img = Image.open(os.path.join(source_dir,image_fn)) - target = int( int(image_fn.split('seed')[1].replace('.jpg','')) > 2500 ) #class 1 seeds=2501-5000 - - if self.transform is not None: - img = self.transform(img) - - return {'image': img, 'target':target} - - -class AdaptiveConcatPool2d(nn.Module): - def __init__(self): - super().__init__() - self.avg = nn.AdaptiveAvgPool2d(output_size=(1, 1)) - self.max = nn.AdaptiveMaxPool2d(output_size=(1, 1)) - - def forward(self, x): - avg_x = self.avg(x) - max_x = self.max(x) - return torch.cat([avg_x, max_x], dim=1) - - -class Flatten(nn.Module): - def forward(self, x): - return x.view(x.shape[0], -1) - - -class Model(nn.Module): - - def __init__(self, c_out=1, arch='resnet34'): - super().__init__() - self.arch = arch - if arch == 'resnet34': - remove_range = 2 - m = models.resnet34(pretrained=True) - #fc = nn.Linear(in_features=512, out_features=500, bias=True) - elif arch == 'efficientnet': - m = EfficientNet.from_pretrained("efficientnet-b6") - m._fc = nn.Linear(in_features=2304, out_features=500, bias=True) - self.base = m - self.head = nn.Linear(500, 1) - #fc = nn.Linear(in_features=1280, out_features=500, bias=True) - elif arch == 'seresnext50': - m = torch.hub.load('facebookresearch/semi-supervised-ImageNet1K-models', 'resnext50_32x4d_ssl') - #fc = nn.Linear(in_features=512, out_features=500, bias=True) - remove_range = 2 - - if arch != 'efficientnet': - c_feature = list(m.children())[-1].in_features - self.base = nn.Sequential(*list(m.children())[:-remove_range]) - self.head = nn.Sequential(AdaptiveConcatPool2d(), - Flatten(), - nn.Linear(c_feature * 2, c_out)) - - - def forward(self, x): - h = self.base(x) - logits = self.head(h).squeeze(1) - return logits - - -class FocalLoss(nn.Module): - def __init__(self, alpha=1, gamma=2, logits=False, reduce=True): - super(FocalLoss, self).__init__() - self.alpha = alpha - self.gamma = gamma - self.logits = logits - self.reduce = reduce - - def forward(self, inputs, targets): - if self.logits: - BCE_loss = F.binary_cross_entropy_with_logits(inputs, targets, reduction='none') - else: - BCE_loss = F.binary_cross_entropy(inputs, targets, reduction='none') - pt = torch.exp(-BCE_loss) - F_loss = self.alpha * (1-pt)**self.gamma * BCE_loss - - if self.reduce: - return torch.mean(F_loss) - else: - return F_loss - - -class LightModel(pl.LightningModule): - - def __init__(self, df_train, df_test, pid_train, pid_val, test_syn=False): - # This is where paths and options should be stored. I also store the - # train_idx, val_idx for cross validation since the dataset are defined - # in the module ! - super().__init__() - self.pid_train = pid_train - self.pid_val = pid_val - self.df_train = df_train - - self.model = Model(arch=hparams.arch) # You will obviously want to make the model better :) - - - # Defining datasets here instead of in prepare_data usually solves a lot of problems for me... - self.transform_train = transforms.Compose([#transforms.Resize((224, 224)), # Use this when training with original images - transforms.RandomHorizontalFlip(0.5), - transforms.RandomVerticalFlip(0.5), - transforms.ToTensor(), - transforms.Normalize(mean=[0.485, 0.456, 0.406], - std=[0.229, 0.224, 0.225])]) - self.transform_test = transforms.Compose([#transforms.Resize((224, 224)), # Use this when training with original images - transforms.ToTensor(), - transforms.Normalize(mean=[0.485, 0.456, 0.406], - std=[0.229, 0.224, 0.225])]) - - if test_syn: - self.testset = Synth_Dataset(self.transform_test, test=True) - else: - self.trainset = SIIMDataset(self.df_train[self.df_train['patient_id'].isin(pid_train)], self.transform_train) - self.valset = SIIMDataset(self.df_train[self.df_train['patient_id'].isin(pid_val)], self.transform_test) - self.testset = SIIMDataset(df_test, self.transform_test, test=True) - - def forward(self, batch): - # What to do with a batch in a forward. Usually simple if everything is already defined in the model. - return self.model(batch['image']) - - def prepare_data(self): - # This is called at the start of training - pass - - def train_dataloader(self): - # Simply define a pytorch dataloader here that will take care of batching. Note it works well with dictionnaries ! - train_dl = tdata.DataLoader(self.trainset, batch_size=hparams.batch_size, shuffle=True, - num_workers=os.cpu_count()) - return train_dl - - def val_dataloader(self): - # Same but for validation. Pytorch lightning allows multiple validation dataloaders hence why I return a list. - val_dl = tdata.DataLoader(self.valset, batch_size=hparams.batch_size, shuffle=False, - num_workers=os.cpu_count()) - return [val_dl] - - def test_dataloader(self): - test_dl = tdata.DataLoader(self.testset, batch_size=hparams.batch_size, shuffle=False, - num_workers=os.cpu_count()) - return [test_dl] - - - def loss_function(self, logits, gt): - # How to calculate the loss. Note this method is actually not a part of pytorch lightning ! It's only good practice - if self.loss == 'bce': - loss_fn = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([32542/584]).to(logits.device)) # Let's rebalance the weights for each class here. - elif self.loss == 'focal': - loss_fn = FocalLoss(logits=True) - gt = gt.float() - loss = loss_fn(logits, gt) - return loss - - def configure_optimizers(self): - # Optimizers and schedulers. Note that each are in lists of equal length to allow multiple optimizers (for GAN for example) - optimizer = torch.optim.Adam(self.model.parameters(), lr=hparams.lr, weight_decay=3e-6) - scheduler = torch.optim.lr_scheduler.OneCycleLR(optimizer, max_lr=10 * hparams.lr, - epochs=hparams.epochs, steps_per_epoch=len(self.train_dataloader())) - # torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer=optimizer, mode='max', patience=1, verbose=True, factor=0.2) - return [optimizer], [scheduler] - - def training_step(self, batch, batch_idx): - # This is where you must define what happens during a training step (per batch) - logits = self(batch) - loss = self.loss_function(logits, batch['target']).unsqueeze(0) # You need to unsqueeze in case you do multi-gpu training - # Pytorch lightning will call .backward on what is called 'loss' in output - # 'log' is reserved for tensorboard and will log everything define in the dictionary - return {'loss': loss, 'log': {'train_loss': loss}} - - def validation_step(self, batch, batch_idx): - # This is where you must define what happens during a validation step (per batch) - logits = self(batch) - loss = self.loss_function(logits, batch['target']).unsqueeze(0) - probs = torch.sigmoid(logits) - return {'val_loss': loss, 'probs': probs, 'gt': batch['target']} - - def test_step(self, batch, batch_idx): - logits = self(batch) - probs = torch.sigmoid(logits) - - predicted = torch.round(probs) - loss = self.loss_function(logits, batch['target']).unsqueeze(0) - return {'prediction': predicted, 'probs': probs, 'gt': batch['target']} - - def validation_epoch_end(self, outputs): - # This is what happens at the end of validation epoch. Usually gathering all predictions - # outputs is a list of dictionary from each step. - avg_loss = torch.cat([out['val_loss'] for out in outputs], dim=0).mean() - probs = torch.cat([out['probs'] for out in outputs], dim=0) - gt = torch.cat([out['gt'] for out in outputs], dim=0) - probs = probs.detach().cpu().numpy() - gt = gt.detach().cpu().numpy() - - auc_roc = torch.tensor(roc_auc_score(gt, probs)) - tensorboard_logs = {'val_loss': avg_loss, 'auc': auc_roc} - print(f'Epoch {self.current_epoch}: {avg_loss:.2f}, auc: {auc_roc:.4f}') - - return {'avg_val_loss': avg_loss, 'log': tensorboard_logs} - - def test_epoch_end(self, outputs): - probs = torch.cat([out['probs'] for out in outputs], dim=0) - gt = torch.cat([out['gt'] for out in outputs], dim=0) - predicted = torch.cat([out['predicted'] for out in outputs], dim=0) - probs = probs.detach().cpu().numpy() - gt = gt.detach().cpu().numpy() - predicted = predicted.detach().cpu().numpy() - - total = gt.size(0) - correct = (predicted == gt).sum().item() - - print('Accuracy of the network on the test images: %d %%' % ( 100 * correct / total)) - - self.test_predicts = probs # Save prediction internally for easy access - - auc_roc = torch.tensor(roc_auc_score(gt, probs)) - print(f'auc: {auc_roc:.4f}') - # We need to return something - return {'acc': ( 100 * correct / total), 'auc': auc_roc} - - -training_dir = Path('/home/Data/melanoma_external_256') -train_df = pd.read_csv(training_dir/'train.csv') -test_synth_dir = Path('./generated') -test_df = pd.read_csv(training_dir/'test.csv') -IMAGE_DIR = Path(training_dir) -source_dir = '/home/stylegan2-ada-pytorch/generated' -#frames=[train_df, test_df] -#joint_df = pd.concat(frames) - -""" -labels_list = [] -for n in range(len(train_df)): - labels_list.append([train_df.iloc[n].image_name,int(train_df.iloc[n].target)]) -labels_list_dict = { "labels" : labels_list} - -with open("labels.json", "w") as outfile: - json.dump(labels_list_dict, outfile) """ - - -# So you have patients that have multiple images. Also apparently the data is imbalanced. Let's verify: -#train_df.groupby(['target']).count() -# so we have approx 60 times more negatives than positives. We need to make sure we split good/bad patients equally. - -#df = pd.read_csv('/kaggle/input/melanoma-external-malignant-256/train_concat.csv') -patient_means = train_df.groupby(['patient_id'])['target'].mean() -patient_ids = train_df['patient_id'].unique() - -# Now let's make our split -train_idx, val_idx = train_test_split(np.arange(len(patient_ids)), stratify=(patient_means > 0), test_size=0.2) # KFold + averaging should be much better considering how small the dataset is for malignant cases - #train_test_split(df, stratify=df.target, test_size = 0.2, random_state=42) - -pid_train = patient_ids[train_idx] -pid_val = patient_ids[val_idx] - -# dict_to_args is a simple helper to make args act like args from argparse. This makes it trivial to then use argparse -OUTPUT_DIR = './lightning_logs' - - -# For training we just need to instantiate the pytorch lightning module and a trainer with a few options. Most importantly this is where you specify how many GPU to use (or TPU) and if you want to do mixed precision training (with apex). For the purpose of this kernel I just do FP32 1GPU training but please read the pytorch lightning doc if you want to try TPU and/or mixed precision. - - - -def main(args: Namespace): - tb_logger = pl.loggers.TensorBoardLogger(save_dir='./', - name=f'baseline', # This will create different subfolders for your models - version=f'0') # If you use KFold you can specify here the fold number like f'fold_{fold+1}' - checkpoint_callback = pl.callbacks.ModelCheckpoint(filename=tb_logger.log_dir + "/{epoch:02d}-{auc:.4f}", - monitor='auc', mode='max') - # Define trainer - trainer = pl.Trainer(max_epochs=args.epochs, auto_lr_find=False, # Usually the auto is pretty bad. You should instead plot and pick manually. - gradient_clip_val=1, - num_sanity_val_steps=0, # Comment that out to reactivate sanity but the ROC will fail if the sample has only class 0 - checkpoint_callback=checkpoint_callback, - gpus=1, - progress_bar_refresh_rate=0 - ) - - if args.test: - model = LightModel(train_df, test_df, pid_train, pid_val, test_syn=True) - trainer = pl.Trainer(resume_from_checkpoint=args.ckpt, gpus=1) - trainer.test(model) - - else: - model = LightModel(train_df, test_df, pid_train, pid_val) - print('TRAINING STARTING...') - trainer.fit(model) - print('TRAINING FINISHED') - - # Grab best checkpoint file - out = Path(tb_logger.log_dir) - aucs = [ckpt.stem[-4:] for ckpt in out.iterdir()] - best_auc_idx = aucs.index(max(aucs)) - best_ckpt = list(out.iterdir())[best_auc_idx] - print('TEST: Using ', best_ckpt) - trainer = pl.Trainer(resume_from_checkpoint=best_ckpt, gpus=1) - - trainer.test(model) - - - preds = model.test_predicts - test_df['target'] = preds - submission = test_df[['image_name', 'target']] - submission.to_csv('submission.csv', index=False) - - -if __name__ == '__main__': - parser = ArgumentParser() - parser.add_argument("--epochs", type=int, default=10, help='Number of training epochs') - parser.add_argument("--batch_size", type=int, default=64) - parser.add_argument("--lr", type=float, default=1e-4) - parser.add_argument("--test", action='store_true', help='Only testing') - parser.add_argument("--arch", type=str, default='efficientnet', help='Choose architecture', - choices=['seresnext50', 'resnet34', 'efficientnet' ]) - parser.add_argument("--loss", type=str, default='focal', help='Choose loss function', - choices=['bce', 'focal']) - parser.add_argument("--ckpt", type=str, default='./training_runs/00000--cond-mirror-auto2/epoch=09-auc=0.8981.ckpt', help='CKPT path for testing') - hparams = parser.parse_args() - - main(hparams) \ No newline at end of file diff --git a/utils.py b/utils.py index 6c28ff6..617f8ae 100644 --- a/utils.py +++ b/utils.py @@ -1,5 +1,5 @@ import numpy as np -import os +import os import cv2 import PIL.Image as Image from matplotlib import pylab as P @@ -8,12 +8,12 @@ import torch.nn.functional as F from torch import nn from torch.utils.data import Dataset -import torchvision +import torchvision import pandas as pd import seaborn as sb import datetime -from sklearn.model_selection import train_test_split -from sklearn.metrics import accuracy_score, roc_auc_score, confusion_matrix, f1_score +from sklearn.model_selection import train_test_split +from sklearn.metrics import accuracy_score, roc_auc_score, confusion_matrix, f1_score from pathlib import Path import random @@ -22,147 +22,156 @@ device = torch.device("cuda" if torch.cuda.is_available() else "cpu") -transformer = torchvision.transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) +transformer = torchvision.transforms.Normalize( + [0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) classes = ('benign', 'melanoma') # Defining transforms for the training, validation, and testing sets -training_transforms = torchvision.transforms.Compose([#Microscope(), - #AdvancedHairAugmentation(), - torchvision.transforms.RandomRotation(30), - #transforms.RandomResizedCrop(256, scale=(0.8, 1.0)), - torchvision.transforms.RandomHorizontalFlip(), - torchvision.transforms.RandomVerticalFlip(), - #transforms.ColorJitter(brightness=32. / 255.,saturation=0.5,hue=0.01), - torchvision.transforms.ToTensor(), - torchvision.transforms.Normalize([0.485, 0.456, 0.406], - [0.229, 0.224, 0.225])]) - -validation_transforms = torchvision.transforms.Compose([torchvision.transforms.Resize(256), - torchvision.transforms.CenterCrop(256), - torchvision.transforms.ToTensor(), - torchvision.transforms.Normalize([0.485, 0.456, 0.406], - [0.229, 0.224, 0.225])]) - -testing_transforms = torchvision.transforms.Compose([torchvision.transforms.Resize(256), - torchvision.transforms.CenterCrop(256), - torchvision.transforms.ToTensor(), - torchvision.transforms.Normalize([0.485, 0.456, 0.406], - [0.229, 0.224, 0.225])]) +training_transforms = torchvision.transforms.Compose( + [torchvision.transforms.RandomRotation(30), + torchvision.transforms.RandomHorizontalFlip(), + torchvision.transforms.RandomVerticalFlip(), + torchvision.transforms.ToTensor(), + torchvision.transforms.Normalize([0.485, 0.456, 0.406], + [0.229, 0.224, 0.225])]) + +validation_transforms = torchvision.transforms.Compose( + [torchvision.transforms.Resize(256), + torchvision.transforms.CenterCrop(256), + torchvision.transforms.ToTensor(), + torchvision.transforms.Normalize([0.485, 0.456, 0.406], + [0.229, 0.224, 0.225])]) + +testing_transforms = torchvision.transforms.Compose( + [torchvision.transforms.Resize(256), + torchvision.transforms.CenterCrop(256), + torchvision.transforms.ToTensor(), + torchvision.transforms.Normalize([0.485, 0.456, 0.406], + [0.229, 0.224, 0.225])]) + def seed_worker(worker_id): worker_seed = torch.initial_seed() % 2**32 np.random.seed(worker_seed) random.seed(worker_seed) + # Creating seeds to make results reproducible def seed_everything(seed_value): np.random.seed(seed_value) random.seed(seed_value) torch.manual_seed(seed_value) os.environ['PYTHONHASHSEED'] = str(seed_value) - - if torch.cuda.is_available(): + + if torch.cuda.is_available(): torch.cuda.manual_seed(seed_value) torch.cuda.manual_seed_all(seed_value) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False - def process_image(image_path): - ''' Scales, crops, and normalizes a PIL image for a PyTorch model, - returns an Numpy array - ''' + ''' + Scales, crops, and normalizes a PIL image for a PyTorch model, + returns an Numpy array + ''' # Process a PIL image for use in a PyTorch model - + pil_image = Image.open(image_path) - + # Resize if pil_image.size[0] > pil_image.size[1]: pil_image.thumbnail((5000, 256)) else: pil_image.thumbnail((256, 5000)) - - # Crop + + # Crop left_margin = (pil_image.width-256)/2 bottom_margin = (pil_image.height-256)/2 right_margin = left_margin + 256 top_margin = bottom_margin + 256 - - pil_image = pil_image.crop((left_margin, bottom_margin, right_margin, top_margin)) - + + pil_image = pil_image.crop( + (left_margin, bottom_margin, right_margin, top_margin)) + # Normalize np_image = np.array(pil_image)/255 mean = np.array([0.485, 0.456, 0.406]) std = np.array([0.229, 0.224, 0.225]) np_image = (np_image - mean) / std - - # PyTorch expects the color channel to be the first dimension but it's the third dimension in the PIL image and Numpy array - # Color channel needs to be first; retain the order of the other two dimensions. + + # PyTorch expects the color channel to be the + # first dimension but it's the third dimension + # in the PIL image and Numpy array + # Color channel needs to be first; + # retain the order of the other two dimensions. np_image = np_image.transpose((2, 0, 1)) - + return np_image + def imshow(image, ax=None, title=None): if ax is None: - fig, ax = plt.subplots() - + _, ax = plt.subplots() + # PyTorch tensors assume the color channel is the first dimension # but matplotlib assumes is the third dimension image = image.transpose((1, 2, 0)) - + # Undo preprocessing mean = np.array([0.485, 0.456, 0.406]) std = np.array([0.229, 0.224, 0.225]) image = std * image + mean - + if title is not None: ax.set_title(title) - - # Image needs to be clipped between 0 and 1 or it looks like noise when displayed + + # Image needs to be clipped between 0 and 1 + # or it looks like noise when displayed image = np.clip(image, 0, 1) - ax.imshow(image) - + return ax -def predict(image_path, model, topk=1): #just 2 classes from 1 single output - ''' Predict the class (or classes) of an image using a trained deep learning model. - ''' + +def predict(image_path, model, topk=1): + # just 2 classes from 1 single output + ''' + Predict the class (or classes) of an image using + a trained deep learning model. + ''' image = process_image(image_path) - + # Convert image to PyTorch tensor first image = torch.from_numpy(image).type(torch.cuda.FloatTensor) - #print(image.shape) - #print(type(image)) - - # Returns a new tensor with a dimension of size one inserted at the specified position. + + # Returns a new tensor with a dimension of size + # one inserted at the specified position. image = image.unsqueeze(0) - output = model(image) - probabilities = torch.sigmoid(output) - - # Probabilities and the indices of those probabilities corresponding to the classes + + # Probabilities and the indices of those probabilities + # corresponding to the classes top_probabilities, top_indices = probabilities.topk(topk) - + # Convert to lists - top_probabilities = top_probabilities.detach().type(torch.FloatTensor).numpy().tolist()[0] - top_indices = top_indices.detach().type(torch.FloatTensor).numpy().tolist()[0] - + top_probabilities = top_probabilities.detach().type( + torch.FloatTensor).numpy().tolist()[0] + top_indices = top_indices.detach().type( + torch.FloatTensor).numpy().tolist()[0] + top_classes = [] - - if probabilities > 0.5 : + + if probabilities > 0.5: top_classes.append("Melanoma") else: top_classes.append("Benign") - return top_probabilities, top_classes - def images_to_probs(net, images): ''' Generates predictions and corresponding probabilities from a trained @@ -172,15 +181,18 @@ def images_to_probs(net, images): # convert output probabilities to predicted class _, preds_tensor = torch.max(output, 1) preds = np.squeeze(preds_tensor.cpu().numpy()) - return preds, [F.softmax(el, dim=0)[i].item() for i, el in zip(preds, output)] + return preds, [ + F.softmax(el, dim=0)[i].item() for i, el in zip(preds, output)] + def renormalize(tensor): - minFrom= tensor.min() - maxFrom= tensor.max() + minFrom = tensor.min() + maxFrom = tensor.max() minTo = 0 - maxTo=1 + maxTo = 1 return minTo + (maxTo - minTo) * ((tensor - minFrom) / (maxFrom - minFrom)) + def matplotlib_imshow(img, one_channel=False): if one_channel: img = img.mean(dim=0) @@ -191,6 +203,7 @@ def matplotlib_imshow(img, one_channel=False): else: plt.imshow(np.transpose(npimg, (1, 2, 0))) + def plot_classes_preds(net, images, labels): ''' Generates matplotlib Figure using a trained network, along with images @@ -209,11 +222,14 @@ def plot_classes_preds(net, images, labels): classes[preds[idx]], probs[idx] * 100.0, classes[labels[idx]]), - color=("green" if preds[idx]==labels[idx].item() else "red")) + color=( + "green" if preds[idx] == labels[idx].item( + ) else "red")) return fig -def add_pr_curve_tensorboard(class_index, test_probs, test_label, writer, global_step=0): +def add_pr_curve_tensorboard(class_index, test_probs, + test_label, writer, global_step=0): ''' Takes in a "class_index" from 0 to 9 and plots the corresponding precision-recall curve @@ -224,7 +240,7 @@ def add_pr_curve_tensorboard(class_index, test_probs, test_label, writer, global else: tensorboard_probs = test_probs - writer.add_pr_curve(classes[class_index], + writer.add_pr_curve(classes[class_index], tensorboard_truth, tensorboard_probs, global_step=global_step) @@ -236,19 +252,20 @@ def confussion_matrix(test, test_pred, test_accuracy, writer_path): cm = confusion_matrix(test, pred) cm_df = pd.DataFrame(cm, - index = ['Benign','Malignant'], - columns = ['Benign','Malignant']) + index=['Benign', 'Malignant'], + columns=['Benign', 'Malignant']) - fig = plt.figure(figsize=(5.5,4)) + plt.figure(figsize=(5.5, 4)) sb.heatmap(cm_df, annot=True) plt.title('Confusion Matrix \nAccuracy:{0:.3f}'.format(test_accuracy)) plt.ylabel('True label') plt.xlabel('Predicted label') plt.show() - now=datetime.datetime.now() - # writer.add_image('conf_matrix', fig) - plt.savefig(os.path.join(writer_path, f'conf_matrix_{test_accuracy:.4f}_{now.strftime("%d_%m_%H_%M")}.png')) + now = datetime.datetime.now() + plt.savefig(os.path.join( + writer_path, + f'conf_matrix_{test_accuracy:.4f}_{now.strftime("%d_%m_%H_%M")}.png')) def ShowImage(im, title='', ax=None): @@ -258,6 +275,7 @@ def ShowImage(im, title='', ax=None): P.imshow(im) P.title(title) + def ShowGrayscaleImage(im, title='', ax=None): if ax is None: P.figure() @@ -265,6 +283,7 @@ def ShowGrayscaleImage(im, title='', ax=None): P.imshow(im, cmap=P.cm.gray, vmin=0, vmax=1) P.title(title) + def ShowHeatMap(im, title, ax=None): if ax is None: P.figure() @@ -272,11 +291,13 @@ def ShowHeatMap(im, title, ax=None): P.imshow(im, cmap='inferno') P.title(title) + def LoadImage(file_path): im = Image.open(file_path) im = np.asarray(im) return im + def PreprocessImages(images): # assumes input is 4-D, with range [0,255] # @@ -285,285 +306,167 @@ def PreprocessImages(images): # https://pytorch.org/vision/stable/models.html images = np.array(images) images = images/255 - images = np.transpose(images, (0,3,1,2)) + images = np.transpose(images, (0, 3, 1, 2)) images = torch.tensor(images, dtype=torch.float32) images = transformer.forward(images).to('cuda') return images.requires_grad_(True) - -def create_split(source_dir, n_b, n_m): - # Split synthetic dataset - input_images = [str(f) for f in sorted(Path(source_dir).rglob('*')) if os.path.isfile(f)] - - ind_0, ind_1 = [], [] - for i, f in enumerate(input_images): - if f.split('.')[0][-1] == '0': - ind_0.append(i) - else: - ind_1.append(i) - - train_id_list, val_id_list = ind_0[:round(len(ind_0)*0.8)], ind_0[round(len(ind_0)*0.8):] #ind_0[round(len(ind_0)*0.6):round(len(ind_0)*0.8)] , - train_id_1, val_id_1 = ind_1[:round(len(ind_1)*0.8)], ind_1[round(len(ind_1)*0.8):] #ind_1[round(len(ind_1)*0.6):round(len(ind_1)*0.8)] , - - train_id_list = np.append(train_id_list, train_id_1) - val_id_list = np.append(val_id_list, val_id_1) - - return train_id_list, val_id_list #test_id_list - - def load_isic_data(path): - # ISIC dataset - df = pd.read_csv(os.path.join(path , 'train_concat.csv')) - # test_df = pd.read_csv(os.path.join(args.data_path ,'melanoma_external_256/test.csv')) - # test_img_dir = os.path.join(args.data_path , 'melanoma_external_256/test/test/') - train_img_dir = os.path.join(path ,'train/train/') - - df['image_name'] = [os.path.join(train_img_dir, df.iloc[index]['image_name'] + '.jpg') for index in range(len(df))] - - train_split, valid_split = train_test_split (df, stratify=df.target, test_size = 0.20, random_state=42) - train_df=pd.DataFrame(train_split) - validation_df=pd.DataFrame(valid_split) + # ISIC dataset + df = pd.read_csv(os.path.join(path, 'train_concat.csv')) + train_img_dir = os.path.join(path, 'train/train/') + + df['image_name'] = [ + os.path.join( + train_img_dir, df.iloc[index]['image_name'] + '.jpg' + ) for index in range(len(df))] + + train_split, valid_split = train_test_split( + df, stratify=df.target, test_size=0.20, random_state=42) + train_df = pd.DataFrame(train_split) + validation_df = pd.DataFrame(valid_split) return train_df, validation_df + def load_synthetic_data(syn_data_path, synt_n_imgs, only_syn=False): - #Load all images and labels from path - input_images = [str(f) for f in sorted(Path(syn_data_path).rglob('*')) if os.path.isfile(f)] + # Load all images and labels from path + input_images = [ + str(f) for f in sorted( + Path(syn_data_path).rglob('*')) if os.path.isfile(f)] y = [0 if f.split('.jpg')[0][-1] == '0' else 1 for f in input_images] - + ind_0, ind_1 = [], [] - for i, f in enumerate(input_images): - if f.split('.')[0][-1] == '0': + for i, _ in enumerate(input_images): + if y[i] == 0: ind_0.append(i) else: - ind_1.append(i) + ind_1.append(i) # Select number of melanomas and benign samples - n_b, n_m = [int(i) for i in synt_n_imgs.split(',') ] if not only_syn else [1000,1000] - ind_0=np.random.permutation(ind_0)[:n_b*1000] - ind_1=np.random.permutation(ind_1)[:n_m*1000] + n_b, n_m = [ + float(i) for i in synt_n_imgs.split(',') + ] if not only_syn else [1000, 1000] + ind_0 = np.random.permutation(ind_0)[:int(n_b * 1000)] + ind_1 = np.random.permutation(ind_1)[:int(n_m * 1000)] - id_list = np.append(ind_0, ind_1) + id_list = np.append(ind_0, ind_1) train_img = [input_images[int(i)] for i in id_list] train_gt = [y[int(i)] for i in id_list] - # train_img, test_img, train_gt, test_gt = train_test_split(input_images, y, stratify=y, test_size=0.2, random_state=3) train_df = pd.DataFrame({'image_name': train_img, 'target': train_gt}) - - return train_df - - -class AdvancedHairAugmentation: - """ - Impose an image of a hair to the target image - Args: - hairs (int): maximum number of hairs to impose - hairs_folder (str): path to the folder with hairs images - """ + return train_df - def __init__(self, hairs: int = 5, hairs_folder: str = "../input/melanoma-hairs"): - self.hairs = hairs - self.hairs_folder = hairs_folder - def __call__(self, img): - """ - Args: - img (PIL Image): Image to draw hairs on. - - Returns: - PIL Image: Image with drawn hairs. - """ - n_hairs = random.randint(0, self.hairs) - - if not n_hairs: - return img - - height, width, _ = img.shape # target image width and height - hair_images = [im for im in os.listdir(self.hairs_folder) if 'png' in im] - - for _ in range(n_hairs): - hair = cv2.imread(os.path.join(self.hairs_folder, random.choice(hair_images))) - hair = cv2.flip(hair, random.choice([-1, 0, 1])) - hair = cv2.rotate(hair, random.choice([0, 1, 2])) - - h_height, h_width, _ = hair.shape # hair image width and height - roi_ho = random.randint(0, img.shape[0] - hair.shape[0]) - roi_wo = random.randint(0, img.shape[1] - hair.shape[1]) - roi = img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width] - - # Creating a mask and inverse mask - img2gray = cv2.cvtColor(hair, cv2.COLOR_BGR2GRAY) - ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY) - mask_inv = cv2.bitwise_not(mask) - - # Now black-out the area of hair in ROI - img_bg = cv2.bitwise_and(roi, roi, mask=mask_inv) - - # Take only region of hair from hair image. - hair_fg = cv2.bitwise_and(hair, hair, mask=mask) - - # Put hair in ROI and modify the target image - dst = cv2.add(img_bg, hair_fg) - - img[roi_ho:roi_ho + h_height, roi_wo:roi_wo + h_width] = dst - - return img - - def __repr__(self): - return f'{self.__class__.__name__}(hairs={self.hairs}, hairs_folder="{self.hairs_folder}")' - -class DrawHair: - """ - Draw a random number of pseudo hairs - - Args: - hairs (int): maximum number of hairs to draw - width (tuple): possible width of the hair in pixels - """ - - def __init__(self, hairs:int = 4, width:tuple = (1, 2)): - self.hairs = hairs - self.width = width - - def __call__(self, img): - """ - Args: - img (PIL Image): Image to draw hairs on. - - Returns: - PIL Image: Image with drawn hairs. - """ - if not self.hairs: - return img - - width, height, _ = img.shape - - for _ in range(random.randint(0, self.hairs)): - # The origin point of the line will always be at the top half of the image - origin = (random.randint(0, width), random.randint(0, height // 2)) - # The end of the line - end = (random.randint(0, width), random.randint(0, height)) - color = (0, 0, 0) # color of the hair. Black. - cv2.line(img, origin, end, color, random.randint(self.width[0], self.width[1])) - - return img - - def __repr__(self): - return f'{self.__class__.__name__}(hairs={self.hairs}, width={self.width})' - -class Microscope: - """ - Cutting out the edges around the center circle of the image - Imitating a picture, taken through the microscope - - Args: - p (float): probability of applying an augmentation - """ - - def __init__(self, p: float = 0.5): - self.p = p - - def __call__(self, img): - """ - Args: - img (PIL Image): Image to apply transformation to. +def create_split(source_dir): + # Split synthetic dataset + input_images = [ + str(f) for f in sorted( + Path(source_dir).rglob('*')) if os.path.isfile(f)] - Returns: - PIL Image: Image with transformation. - """ - if random.random() < self.p: - circle = cv2.circle((np.ones(img.shape) * 255).astype(np.uint8), # image placeholder - (img.shape[0]//2, img.shape[1]//2), # center point of circle - random.randint(img.shape[0]//2 - 3, img.shape[0]//2 + 15), # radius - (0, 0, 0), # color - -1) + ind_0, ind_1 = [], [] + for i, f in enumerate(input_images): + if f.split('.')[0][-1] == '0': + ind_0.append(i) + else: + ind_1.append(i) - mask = circle - 255 - img = np.multiply(img, mask) - - return img + train_id_list = ind_0[round(len(ind_0)*0.8):] + val_id_list = ind_0[round(len(ind_0)*0.8):] + train_id_1 = ind_1[:round(len(ind_1)*0.8)] + val_id_1 = ind_1[round(len(ind_1)*0.8):] - def __repr__(self): - return f'{self.__class__.__name__}(p={self.p})' + train_id_list = np.append(train_id_list, train_id_1) + val_id_list = np.append(val_id_list, val_id_1) + return train_id_list, val_id_list class CustomDataset(Dataset): - def __init__(self, df: pd.DataFrame, train: bool = True, transforms= None): + + def __init__(self, df: pd.DataFrame, + train: bool = True, transforms=None): self.df = df self.transforms = transforms self.train = train + def __len__(self): return len(self.df) + def __getitem__(self, index): img_path = self.df.iloc[index]['image_name'] rgb_img = cv2.imread(img_path, 1)[:, :, ::-1] rgb_img = np.float32(rgb_img) / 255 - images =Image.open(img_path) + images = Image.open(img_path) if self.transforms: images = self.transforms(images) - + labels = self.df.iloc[index]['target'] if self.train: - #return images, labels - return torch.tensor(images, dtype=torch.float32), torch.tensor(labels, dtype=torch.float32) - + # return images, labels + return torch.tensor( + images, dtype=torch.float32), torch.tensor( + labels, dtype=torch.float32) else: - #return (images) - return img_path, torch.tensor(images, dtype=torch.float32), torch.tensor(labels, dtype=torch.float32) - + # return (images) + return img_path, torch.tensor( + images, dtype=torch.float32), torch.tensor( + labels, dtype=torch.float32) + class Synth_Dataset(Dataset): - def __init__(self, source_dir, transform, id_list=None, input_img=None, test = False, unbalanced=False): + def __init__(self, source_dir, transform, id_list=None, + input_img=None, test=False, unbalanced=False): self.transform = transform self.source_dir = source_dir - + if input_img is None: - self.input_images = [str(f) for f in sorted(Path(source_dir).rglob('*')) if os.path.isfile(f)] + self.input_images = [str(f) for f in sorted( + Path(source_dir).rglob('*')) if os.path.isfile(f)] else: self.input_images = input_img - + if unbalanced: - ind_0, ind_1 = create_split(source_dir, unbalanced=unbalanced) - ind=np.append(ind_0, ind_1) + ind_0, ind_1 = create_split(source_dir) + ind = np.append(ind_0, ind_1) self.input_images = [self.input_images[i] for i in ind] - self.id_list = id_list if id_list is not None else range(len(self.input_images)) - + self.id_list = id_list if id_list is not None else range( + len(self.input_images)) + if test: if unbalanced: self.input_images = self.input_images[:5954] - + def __len__(self): return len(self.id_list) - - def __getitem__(self, idx): + + def __getitem__(self, idx): idx = self.id_list[idx] - image_fn = self.input_images[idx] #f'{idx:04d}_{idx%2}' + image_fn = self.input_images[idx] img = np.array(Image.open(image_fn)) - target = int(image_fn.split('_')[-1].replace('.jpg','')) - + target = int(image_fn.split('_')[-1].replace('.jpg', '')) + if self.transform is not None: img = self.transform(img) - return torch.tensor(img, dtype=torch.float32), torch.tensor(target, dtype=torch.float32) + return torch.tensor( + img, dtype=torch.float32), torch.tensor( + target, dtype=torch.float32) - -def load_model(model = 'efficientnet-b2'): +def load_model(model='efficientnet-b2'): if "efficientnet" in model: arch = EfficientNet.from_pretrained(model) elif model == "googlenet": arch = torchvision.models.googlenet(pretrained=True) else: arch = torchvision.models.resnet50(pretrained=True) - model = Net(arch=arch).to(device) - return model @@ -573,23 +476,29 @@ def __init__(self, arch, return_feats=False): self.arch = arch self.return_feats = return_feats if 'fgdf' in str(arch.__class__): - self.arch.fc = nn.Linear(in_features=1280, out_features=500, bias=True) - if 'EfficientNet' in str(arch.__class__): - self.arch._fc = nn.Linear(in_features=self.arch._fc.in_features, out_features=500, bias=True) - #self.dropout1 = nn.Dropout(0.2) - else: - self.arch.fc = nn.Linear(in_features=arch.fc.in_features, out_features=500, bias=True) - + self.arch.fc = nn.Linear( + in_features=1280, + out_features=500, bias=True) + if 'EfficientNet' in str(arch.__class__): + self.arch._fc = nn.Linear( + in_features=self.arch._fc.in_features, + out_features=500, bias=True) + else: + self.arch.fc = nn.Linear( + in_features=arch.fc.in_features, + out_features=500, bias=True) self.ouput = nn.Linear(500, 1) - + def forward(self, images): """ - No sigmoid in forward because we are going to use BCEWithLogitsLoss - Which applies sigmoid for us when calculating a loss + No sigmoid in forward because we are going to + use BCEWithLogitsLoss + Which applies sigmoid for us when calculating + a loss """ x = images features = self.arch(x) output = self.ouput(features) if self.return_feats: return features - return output \ No newline at end of file + return output