diff --git a/README.md b/README.md index 04746882f8a1e2e335e89a7e3b6060268439c5bb..44e45dd515e5bad9f0b68fe1ddb8345311753d6d 100644 --- a/README.md +++ b/README.md @@ -101,4 +101,11 @@ Lecturer: Zachary Baum Use existing open-source for visualizations in Jupyter Notebooks [Tutorial][3d_slicer_jupyter] -[3d_slicer_jupyter]: ./tutorials/3d_slicer_jupyter/readme.md \ No newline at end of file +[3d_slicer_jupyter]: ./tutorials/3d_slicer_jupyter/readme.md + + +### Paralell computing using PyTorch +The demo for guest lecture "Paralell computing using PyTorch", by Qianye Yang. +[Tutorial][pytorch_paralell_computing] + +[pytorch_paralell_computing]: ./tutorials/pytorch_paralell_computing/readme.md diff --git a/tutorials/pytorch_paralell_computing/config.py b/tutorials/pytorch_paralell_computing/config.py new file mode 100644 index 0000000000000000000000000000000000000000..3e1ac1e5b3a1799080f7f9c98368554d25f4b4c5 --- /dev/null +++ b/tutorials/pytorch_paralell_computing/config.py @@ -0,0 +1,20 @@ +import argparse +import os +import logging + +parser = argparse.ArgumentParser() +# common options +parser.add_argument('--exp_name', default=None, type=str, help='experiment name you want to add.') +parser.add_argument('--logdir', default='./logs', type=str, help='log dir') + +# Training options +parser.add_argument('--lr', default=1e-4, type=float, help='Learning rate.') +parser.add_argument('--batch_size', default=16, type=int, help='The number of batch size.') +parser.add_argument('--gpu', default='0', type=str, help='id of gpu') +parser.add_argument('--epochs', default=500, type=int, help='The number of iterations.') +parser.add_argument('--save_period', default=5, type=int, help='save period') +parser.add_argument('--model', default='unet', type=str, help='choose model') + +args = parser.parse_args() + +assert args.exp_name is not None, 'experiment name should not be none' diff --git a/tutorials/pytorch_paralell_computing/data.py b/tutorials/pytorch_paralell_computing/data.py new file mode 100644 index 0000000000000000000000000000000000000000..725f1b1f5b4673c61a50431017b439883948e8cf --- /dev/null +++ b/tutorials/pytorch_paralell_computing/data.py @@ -0,0 +1,27 @@ +import torch +import os +import torchvision.transforms.functional as tf +from torch.utils.data import Dataset +from PIL import Image +from glob import glob + + +class BrainTumorSegData(Dataset): + def __init__(self, phase): + assert phase in ['train', 'val', 'test'] + self.phase = phase # train/val/test + self.image_root = './data/' + self.datalist = glob(os.path.join(self.image_root, phase, '*_mask.tif')) + + def __getitem__(self, index): + msk_path = self.datalist[index] + img_path = msk_path.replace('_mask', '') + img_id = os.path.basename(img_path).replace('.tif', '') + + img, msk = Image.open(img_path), Image.open(msk_path) + img, msk = tf.to_tensor(img), tf.to_tensor(msk) + + return img, msk, img_id + + def __len__(self): + return len(self.datalist) diff --git a/tutorials/pytorch_paralell_computing/loss.py b/tutorials/pytorch_paralell_computing/loss.py new file mode 100644 index 0000000000000000000000000000000000000000..0d4c3c3f95b488ac7c43ca2134947bd35c522dee --- /dev/null +++ b/tutorials/pytorch_paralell_computing/loss.py @@ -0,0 +1,27 @@ +import torch.nn as nn +import torch + +class DiceLoss(nn.Module): + def __init__(self): + super(DiceLoss, self).__init__() + self.smooth = 1.0 + + def forward(self, y_pred, y_true): + assert y_pred.size() == y_true.size() + y_pred = y_pred[:, 0].contiguous().view(-1) + y_true = y_true[:, 0].contiguous().view(-1) + intersection = (y_pred * y_true).sum() + dsc = (2. * intersection + self.smooth) / (y_pred.sum() + y_true.sum() + self.smooth) + return 1. - dsc + + +def binary_dice(y_true, y_pred): + eps = 1e-6 + y_true = y_true >= 0.5 + y_pred = y_pred >= 0.5 + numerator = torch.sum(y_true * y_pred) * 2 + denominator = torch.sum(y_true) + torch.sum(y_pred) + if numerator == 0 or denominator == 0: + return 0.0 + else: + return numerator * 1.0 / denominator \ No newline at end of file diff --git a/tutorials/pytorch_paralell_computing/networks.py b/tutorials/pytorch_paralell_computing/networks.py new file mode 100644 index 0000000000000000000000000000000000000000..3cff5c3792b16a024b03a83c29aec469318ddaa8 --- /dev/null +++ b/tutorials/pytorch_paralell_computing/networks.py @@ -0,0 +1,98 @@ +from collections import OrderedDict + +import torch +import torch.nn as nn + + +class UNet(nn.Module): + + def __init__(self, in_channels=3, out_channels=1, init_features=32): + super(UNet, self).__init__() + + features = init_features + self.encoder1 = UNet._block(in_channels, features, name="enc1") + self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2) + self.encoder2 = UNet._block(features, features * 2, name="enc2") + self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2) + self.encoder3 = UNet._block(features * 2, features * 4, name="enc3") + self.pool3 = nn.MaxPool2d(kernel_size=2, stride=2) + self.encoder4 = UNet._block(features * 4, features * 8, name="enc4") + self.pool4 = nn.MaxPool2d(kernel_size=2, stride=2) + + self.bottleneck = UNet._block(features * 8, features * 16, name="bottleneck") + + self.upconv4 = nn.ConvTranspose2d( + features * 16, features * 8, kernel_size=2, stride=2 + ) + self.decoder4 = UNet._block((features * 8) * 2, features * 8, name="dec4") + self.upconv3 = nn.ConvTranspose2d( + features * 8, features * 4, kernel_size=2, stride=2 + ) + self.decoder3 = UNet._block((features * 4) * 2, features * 4, name="dec3") + self.upconv2 = nn.ConvTranspose2d( + features * 4, features * 2, kernel_size=2, stride=2 + ) + self.decoder2 = UNet._block((features * 2) * 2, features * 2, name="dec2") + self.upconv1 = nn.ConvTranspose2d( + features * 2, features, kernel_size=2, stride=2 + ) + self.decoder1 = UNet._block(features * 2, features, name="dec1") + + self.conv = nn.Conv2d( + in_channels=features, out_channels=out_channels, kernel_size=1 + ) + + def forward(self, x): + enc1 = self.encoder1(x) + enc2 = self.encoder2(self.pool1(enc1)) + enc3 = self.encoder3(self.pool2(enc2)) + enc4 = self.encoder4(self.pool3(enc3)) + + bottleneck = self.bottleneck(self.pool4(enc4)) + + dec4 = self.upconv4(bottleneck) + dec4 = torch.cat((dec4, enc4), dim=1) + dec4 = self.decoder4(dec4) + dec3 = self.upconv3(dec4) + dec3 = torch.cat((dec3, enc3), dim=1) + dec3 = self.decoder3(dec3) + dec2 = self.upconv2(dec3) + dec2 = torch.cat((dec2, enc2), dim=1) + dec2 = self.decoder2(dec2) + dec1 = self.upconv1(dec2) + dec1 = torch.cat((dec1, enc1), dim=1) + dec1 = self.decoder1(dec1) + return torch.sigmoid(self.conv(dec1)) + + @staticmethod + def _block(in_channels, features, name): + return nn.Sequential( + OrderedDict( + [ + ( + name + "conv1", + nn.Conv2d( + in_channels=in_channels, + out_channels=features, + kernel_size=3, + padding=1, + bias=False, + ), + ), + (name + "norm1", nn.BatchNorm2d(num_features=features)), + (name + "relu1", nn.ReLU(inplace=True)), + ( + name + "conv2", + nn.Conv2d( + in_channels=features, + out_channels=features, + kernel_size=3, + padding=1, + bias=False, + ), + ), + (name + "norm2", nn.BatchNorm2d(num_features=features)), + (name + "relu2", nn.ReLU(inplace=True)), + ] + ) +) diff --git a/tutorials/pytorch_paralell_computing/preprocessing.py b/tutorials/pytorch_paralell_computing/preprocessing.py new file mode 100644 index 0000000000000000000000000000000000000000..8808dae4ecc4558eaa43cf94b2941bcec88d3ded --- /dev/null +++ b/tutorials/pytorch_paralell_computing/preprocessing.py @@ -0,0 +1,33 @@ +import os +from glob import glob +from PIL import Image +import numpy as np +from shutil import copyfile + + +dest_folder = './data' +os.mkdir(dest_folder) + +def has_tumor(img_path): + img = Image.open(img_path) + img_arr = np.array(img) + return img_arr.max() > 0 + +os.system('unzip ./raw_data/archive.zip -d ./raw_data') + +image_folders = [i for i in glob('./raw_data/kaggle_3m/*') if os.path.isdir(i)] + +train_folders = image_folders[:90] +val_folders = image_folders[90:100] +test_folders = image_folders[100:] + +phases = ['train', 'val', 'test'] +for idx, f in enumerate([train_folders, val_folders, test_folders]): + phase_folder = os.path.join(dest_folder, phases[idx]) + os.mkdir(phase_folder) + for imf in f: + print(f'processing {imf}') + masks = [i for i in glob(os.path.join(imf, '*_mask.tif')) if has_tumor(i)] + imgs = [i.replace('_mask', '') for i in masks] + [copyfile(i, os.path.join(phase_folder, os.path.basename(i))) for i in masks] + [copyfile(i, os.path.join(phase_folder, os.path.basename(i))) for i in imgs] diff --git a/tutorials/pytorch_paralell_computing/readme.md b/tutorials/pytorch_paralell_computing/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..bc6156a5869d1c8e6a2aaf0cfde2014d13160280 --- /dev/null +++ b/tutorials/pytorch_paralell_computing/readme.md @@ -0,0 +1,10 @@ +# Paralell computing using PyTorch + +The demo for guest lecture "Paralell computing using PyTorch", by Qianye Yang. + +## Contents + +[Lecture Video](https://drive.google.com/file/d/1IZ43cJbHuavDUrgFAWeecbDna2x6ElTL/view?usp=sharing) + +[Lecture Slides](https://drive.google.com/file/d/125ERaZRH_v4NJc5ONMULZT64Xa3efYPc/view?usp=sharing) + diff --git a/tutorials/pytorch_paralell_computing/scripts/deeplabv3-resnet50-baselines.sh b/tutorials/pytorch_paralell_computing/scripts/deeplabv3-resnet50-baselines.sh new file mode 100644 index 0000000000000000000000000000000000000000..99221942f7ea85840d189b79769d09049d520e76 --- /dev/null +++ b/tutorials/pytorch_paralell_computing/scripts/deeplabv3-resnet50-baselines.sh @@ -0,0 +1,4 @@ +cd .. + + +python train.py --exp_name deeplabv3_resnet50 --batch_size 8 --epochs 50 --model deeplabv3_resnet50 diff --git a/tutorials/pytorch_paralell_computing/scripts/fcn-resnet50.sh b/tutorials/pytorch_paralell_computing/scripts/fcn-resnet50.sh new file mode 100644 index 0000000000000000000000000000000000000000..0cc5dffa0793a3d8c1eb403d473c8b3da456ba70 --- /dev/null +++ b/tutorials/pytorch_paralell_computing/scripts/fcn-resnet50.sh @@ -0,0 +1,3 @@ +cd .. + +python train.py --exp_name fcn_resnet50 --batch_size 8 --epochs 50 --model fcn_resnet50 diff --git a/tutorials/pytorch_paralell_computing/scripts/unet_baseline.sh b/tutorials/pytorch_paralell_computing/scripts/unet_baseline.sh new file mode 100644 index 0000000000000000000000000000000000000000..934fb274ecaf74719257992a5ba0acf1e8f8e10c --- /dev/null +++ b/tutorials/pytorch_paralell_computing/scripts/unet_baseline.sh @@ -0,0 +1,4 @@ +cd .. + +python train.py --exp_name unet --batch_size 16 --epochs 50 --model unet + diff --git a/tutorials/pytorch_paralell_computing/train.py b/tutorials/pytorch_paralell_computing/train.py new file mode 100644 index 0000000000000000000000000000000000000000..b98e5f9c31a9b5f17b57b9d47a3414259ac516ca --- /dev/null +++ b/tutorials/pytorch_paralell_computing/train.py @@ -0,0 +1,119 @@ +import torch +import torchvision +import logging +import torch.optim as optim +from config import args +from data import BrainTumorSegData +from torch.utils.data import DataLoader +from torch.nn import functional +from loss import DiceLoss, binary_dice +from networks import UNet +import os +import utils + +''' +*** means key steps, otherwise optional. +''' + +# Print the params # +for key, item in args.__dict__.items(): + print(f'{key} : {item}') + +#*** GPU setting ***# +os.environ["CUDA_VISIBLE_DEVICES"] = args.gpu +torch.backends.cudnn.benchmark = True + + +# Set a model # +if args.model=='unet': + model = UNet(in_channels=3, out_channels=1) +elif args.model=='deeplabv3_resnet50': + model = torchvision.models.segmentation.deeplabv3_resnet50(num_classes=2) +elif args.model=='fcn_resnet50': + model = torchvision.models.segmentation.fcn_resnet50(num_classes=2) +else: + raise ValueError("wrong model") + +#*** GPU setting ***# +model.cuda() + +# Get outputs from a model # +def get_output(args, model, x): + if args.model=='unet': + out = model(x) + elif args.model=='deeplabv3_resnet50': + out = model(x)['out'][:, 1:2] + out = functional.sigmoid(out) + elif args.model=='fcn_resnet50': + out = model(x)['out'][:, 1:2] + out = functional.sigmoid(out) + else: + pass + return out + +#*** Data Loaders ***# +trainset = BrainTumorSegData(phase='train') +trainloader = DataLoader(trainset, batch_size=args.batch_size, shuffle=True, num_workers=4) +valset = BrainTumorSegData(phase='val') +valloader = DataLoader(valset, batch_size=1, shuffle=False, num_workers=4) +testset = BrainTumorSegData(phase='test') +testloader = DataLoader(testset, batch_size=1, shuffle=False, num_workers=4) + +#*** Set optimizer and loss function ***# +optimizer = optim.Adam(model.parameters(), lr=args.lr) +dice_loss = DiceLoss() + + +#*** Train and validation loop ***# +best = 0 +for epoch in range(args.epochs): + model.train() + for step, (img, msk, img_id) in enumerate(trainloader): + img, msk = img.cuda(), msk.cuda() + optimizer.zero_grad() + out = get_output(args, model, img) + loss = dice_loss(out, msk) + loss.backward() + optimizer.step() + print(f'epoch {epoch}, {step*args.batch_size}/{len(trainset)}, loss={loss:.3f}') + + model.eval() + with torch.no_grad(): + print('in validation...') + losses = [] + for idx, (img, msk, img_id) in enumerate(valloader): + img, msk = img.cuda(), msk.cuda() + out = get_output(args, model, img) + dsc = binary_dice(msk, out) + losses.append(dsc) + avg = torch.mean(torch.tensor(losses)).cpu().numpy() + std = torch.std(torch.tensor(losses)).cpu().numpy() + print(avg, std) + if avg > best: + best = avg + best_epoch = epoch + best_model_path = utils.save_model(model, args, model_name='best.pth') + if (epoch+1)%args.save_period==0: + model_path = utils.save_model(model, args, model_name=f'epoch-{epoch}.pth') + + +#*** Test ***# +with torch.no_grad(): + print('final test...') + print(f'inference with best model from epoch {best_epoch}') + model = torch.load(best_model_path) + model = model.cuda() + losses = [] + for idx, (img, msk, img_id) in enumerate(testloader): + img, msk = img.cuda(), msk.cuda() + out = get_output(args, model, img) + dsc = binary_dice(msk, out) + losses.append(dsc) + print(idx, img_id[0], f'dsc:{dsc:.3f}') + + save_path = os.path.join('./vis', args.exp_name, str(best_epoch)) + os.makedirs(save_path, exist_ok=True) + utils.save_img(out, img, msk, save_path, img_id) + avg = torch.mean(torch.tensor(losses)).cpu().numpy() + std = torch.std(torch.tensor(losses)).cpu().numpy() + print(f'DICE SCORE, Mean: {avg}, Std:{std}') diff --git a/tutorials/pytorch_paralell_computing/utils.py b/tutorials/pytorch_paralell_computing/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f04600d48214ea6a803f9940e9d05c8e34ecee28 --- /dev/null +++ b/tutorials/pytorch_paralell_computing/utils.py @@ -0,0 +1,25 @@ +import torch +import os +import numpy as np +import matplotlib.pyplot as plt + +def save_model(model, args, model_name): + + exp_name = args.exp_name + os.makedirs(os.path.join(args.logdir, exp_name), exist_ok=True) + model_path = os.path.join(args.logdir, exp_name, model_name) + print(f'saving model to {model_path}') + torch.save(model, model_path) + return model_path + +def save_img(out, img, msk, save_path, img_id): + out = out.cpu().numpy()[0, 0, :, :] + out = np.concatenate([out[..., None]]*3, axis=2) + + img = img.cpu().numpy()[0, :, :, :] + img = np.transpose(img, (1, 2, 0)) + + msk = msk.cpu().numpy()[0, 0, :, :] + msk = np.concatenate([msk[..., None]]*3, axis=2) + cc = np.concatenate([img, msk, out], axis=1) + plt.imsave(os.path.join(save_path, img_id[0] + '.png'), cc) \ No newline at end of file