| SOCR ≫ | DSPA ≫ | DSPA2 Topics ≫ |
This DSPA2 module represents Part 3 of the DSPA2 Chapter 14 (Deep Learning, Neural Networks). Learners are encouraged to first complete Part 1 of the DSPA2 Chapter 14 (Deep Learning, Neural Networks), Part 2 of the DSPA2 Chapter 14 (Deep Learning, Neural Networks), and Part 3 of the DSPA2 Chapter 14 (Deep Learning, Neural Networks) prior to continuing with this Part 4 of the DSPA2 Chapter 14 (Deep Learning, Neural Networks), which covers the Torch and Tensorflow Image Pre-processing Image Classification Pipelines.
Torch Deep
Convolutional Neural Network (CNN)The U-Net:
Convolutional Networks for Biomedical Image Segmentation, shown on
the image below, is an example of a DCNN. The U-shaped CNN (U-Net) represents
successive convolutional layers with max-pooling. During the
auto-encoding (left-down-hill branch) the U-Net reduces image resolution
(downsampling), whereas during the subsequent decoding phase
(right-uphill branch) upsamples the images to arrive at an output of the
same size as the original input. The information analysis (encoding) and
synthesis (decoding) facilitate the labeling of each output image pixel
by feeding information in each decoding layer from the corresponding
encoding layer with matching resolution in the downsizing encoding
layer.
Each upsampling (decoding) step concatenates the output from the previous layer with that from its counterpart in the compression (encoding) step. The final decoding output is a mask of the same size as the original image, derived by a \(1\times 1\)-convolution, which does not require a dense layer at the end as the output convolutional layer represents a single filter. Below we show how to load, train, and use a U-Net for transfer learning in 2D image segmentation. Note that this model has over \(3M\) trainable parameters. You can see an R example of a Unet model for input-output tensors of shape=c(128,128), see lines 73-183.
# If necessary, download the U-Net package, before you load it into R
# remotes::install_github("r-tensorflow/unet")
library(tfdatasets)
library(tfds)
library(tfhub)
library(tfruns)
library(torch)
# torch::install_torch()
# remotes::install_github("r-tensorflow/unet")
library(unet)
library(tibble)
# The u-Net call takes additional parameters, e.g., number of downsizing blocks, number of filters to start with,
# number of classes to identify; # ?unet provides details. For instance, we can specify the shape
# of the input images we will be segmenting tumors for: 256*256 3-channel RGB images.
model <- unet::unet(input_shape = c(256, 256, 3))
# to print the model as text output, run:
# model
# Results: # Trainable params: 31,031,745Let’s first download and load in the Brain Tumor Imaging dataset. These data come from a 2019 study on Association of genomic subtypes of lower-grade gliomas with shape features automatically extracted by a deep learning algorithm. The 2D brain MR images are paired with 2D tumor masks, which are trivial for controls and non-trivial for patients, using The Cancer Imaging Archive (TCIA). The data represent 110 patients with lower-grade glioma and include fluid-attenuated inversion recovery (FLAIR) MRI scans. There are 3-channels of the MRI data; pre-contrast, FLAIR, and post-contrast. The corresponding tumor masks were obtained by manual-delineations on the FLAIR images by board-certified radiologists.
# If you need to start a clean fresh run, remove all old files first! Be careful with this! set eval=T in all R-blocks!
##### First check > list.files("/data/")
##### do.call(file.remove, list(list.files("/data", full.names = TRUE)))
##### unlink("/data/*", recursive=TRUE, force=TRUE)
library(httr)
pathToZip <- tempfile()
pathToZip<-paste0(pathToZip,".zip")
#
#
url <- "https://umich.instructure.com/files/21813670/download?download_frd=1"
response <- GET(url)
content_type <- http_type(response)
print(content_type)
if (content_type == "application/zip" || content_type == "application/x-zip-compressed") {
content <- content(response, "raw")
writeBin(content, pathToZip)
} else {
stop("Unexpected content type received.")
}
# download.file("https://umich.instructure.com/files/21813670/download?download_frd=1", pathToZip, mode = "wb")
zip::unzip(pathToZip, files=NULL, exdir = paste0(getwd(),'/data'))library(tibble)
library(rsample)
train_dir <- file.path(getwd(),"data","data")
valid_dir <- file.path(getwd(),"data","mri_valid")
library(magick) # Needed for TIFF --> PNG image conversion and other image processing tasksCreate the necessary directories to store the training and validation imaging data (brain MRIs and tumor masks).
# check if ReadMe file is accessible
# file.rename("/data/ReadMe_TCGA_MRI_Segmentation_Data_Phenotypes.txt", train_dir)
# Import the meta-data
# meta_data <- read.csv(paste0(getwd(),"//data//TCGA_MRI_Segmentation_Data_Phenotypes.csv"))
file_path <- file.path(getwd(), "data", "TCGA_MRI_Segmentation_Data_Phenotypes.csv")
# Read the CSV file
meta_data <- read.csv(file_path)
# note that these are relative file/directory names. To see the complete local path
# tempdir(); getwd()
# Create a validation folder
dir.create(valid_dir)
# Check all n=110 patients are accessible
patients <- list.dirs(train_dir, recursive = FALSE)
length(patients)
# Randomly select 20 Patients for validation, remaining 90=110-20 are for training the DNN model
valid_indices <- sample(1:length(patients), 20)
valid_indices
patients[valid_indices] # prints the actual folders where the validation participants' data is
# Extract and Relocate the Validation cases (separate them from training data)
for (i in valid_indices) {
dir.create(file.path(valid_dir, basename(patients[i])))
for (f in list.files(patients[i])) {
file.rename(file.path(train_dir, basename(patients[i]), f), file.path(valid_dir, basename(patients[i]), f))
}
unlink(file.path(train_dir, basename(patients[i])), recursive = TRUE) # clean
}
# Confirm that only 80 patients are left in the standard data folder
# list all training data imaging files: list.dirs(train_dir, recursive = FALSE)
length(list.dirs(train_dir, recursive = FALSE))
# and 30-60 validation cases are in the validation folder
length(list.dirs(valid_dir, recursive = FALSE))
# and check validation data
length(list.files(valid_dir, recursive = T)) # [1] 1268Define data-frames containing the file-names for all training and validation data.
# Identify the TRAINING and VALIDATION data objects (raw images + tumor masks) as filenames
data_train <- tibble(
img = grep(list.files(train_dir, full.names = TRUE, pattern = "tif", recursive = TRUE),
pattern = 'mask', invert = TRUE, value = TRUE),
mask = grep(list.files(train_dir, full.names = TRUE, pattern = "tif", recursive = TRUE),
pattern = 'mask', value = TRUE)
)
data_valid <- tibble(
img = grep(list.files(valid_dir, full.names = TRUE, pattern = "tif", recursive = TRUE),
pattern = 'mask', invert = TRUE, value = TRUE),
mask = grep(list.files(valid_dir, full.names = TRUE, pattern = "tif", recursive = TRUE),
pattern = 'mask', value = TRUE)
)(Optionally) convert all 2D TIFF images to PNG RGB format! This may be necessary to ensure the input images are 3-channels, and are correctly interpreted as tensorflow objects.
# If all training + testing data are in one folder, split them by:
# data <- initial_split(data_train, prop = 0.8)
# convert all Training Data: TIFF images and masks to PNG format (for easier TF processing downstream)
files_img_tif <- data_train$img[grepl("\\.tif$", data_train$img), drop = TRUE]
data_train_img_png <- lapply(files_img_tif,
function(x) {
# image_write(image_read(x), path = gsub(".tif$", ".png", x), format = "png")
a = image_convert(image_read(x), format = "png")
image_write(a, path = gsub(".tif$", ".png", x), format = "png")
}
)
files_mask_tif <- data_train$mask[grepl("\\.tif$", data_train$mask), drop = TRUE]
data_train_mask_png <- lapply(files_mask_tif,
function(x) {
# image_write(image_read(x), path = gsub(".tif$", ".png", x), format = "png")
a = image_convert(image_read(x), format = "png")
image_write(a, path = gsub(".tif$", ".png", x), format = "png")
}
)
# Similarly convert all Validation Data
# convert all TIFF images and masks to PNG format (for easier TF processing downstream)
files_valid_img_tif <- data_valid$img[grepl("\\.tif$", data_valid$img), drop = TRUE]
data_valid_img_png <- lapply(files_valid_img_tif,
function(x) {
# image_write(image_read(x), path = gsub(".tif$", ".png", x), format = "png")
a = image_convert(image_read(x), format = "png")
image_write(a, path = gsub(".tif$", ".png", x), format = "png")
}
)
files_valid_mask_tif <- data_valid$mask[grepl("\\.tif$", data_valid$mask), drop = TRUE]
data_valid_mask_png <- lapply(files_valid_mask_tif,
function(x) {
# image_write(image_read(x), path = gsub(".tif$", ".png", x), format = "png")
a = image_convert(image_read(x), format = "png")
image_write(a, path = gsub(".tif$", ".png", x), format = "png")
}
)
# Check that the TIF --> PNG conversion worked, inspect one case
head(list.files("/data/data/TCGA_HT_A61A_20000127"))
# data_valid # check root directory
# Inspect some of the images/masks
# image_info(image_read(data_train_img_png[[3]]))
# image_write(image_read(data_train$img[3]), format = "tiff")
# image_write(image_read(data_train$img[3]), path = paste0(data_train$img[3], ".png"), format = "png")
# a <- image_read(paste0(data_train$img[3], ".png"))
# list.files(train_dir)
# To clean previous file references
# # delete a directory -- must add recursive = TRUE
# unlink("/data", recursive = TRUE); # Clean space # gc(full=T)Derive a binary class label - cancer (for non-trivial tumor masks) or control (for empty tumor masks).
# Compute a new binary outcome variable 1=Brain Tumor (mask has at least 1 white pixel), 0=Normal Brain, no white pixels in the mask
pos_neg_diagnosis <- sapply(data_train$mask,
function(x) { value = max(imager::magick2cimg(image_read(x)))
ifelse (value > 0, 1, 0) }
)
table(pos_neg_diagnosis) #; head(data_train)## pos_neg_diagnosis
## 0 1
## 2164 1153
# pos_neg_diagnosis
# 0 1
# 2046 1103
# Add the normal vs. cancer label to training and testing datasets
data_train$label <- pos_neg_diagnosis
pos_neg_diagnosis_valid <- sapply(data_valid$mask,
function(x) { value = max(imager::magick2cimg(image_read(x)))
ifelse (value > 0, 1, 0) }
)
table(pos_neg_diagnosis_valid)## pos_neg_diagnosis_valid
## 0 1
## 392 220
Next we will ingest the 3-channel (RGB) imaging data and the
corresponding tumor masks (binary images) for each participant.
The method torch::dataset() allows specifying
initialize() and .getitem() methods for
complex computable data objects. The first method
initialize() creates the archive of imaging and
mask file names that can be utilized by the second method
.getitem() for iterating over all cases. The method
.getitem() returns ordered input-output pairs and performs
weighted sampling, with prevalence to large lesion images, which is
useful for accounting for DNN training with imbalanced classes.
The training sets can be enhanced by data augmentation – a process expanding the set of training images and masks via operations such as flipping, resizing, and rotating based on certain specifications.
Below we use PyTorch to define
a brain_dataset method providing a larger
augmented training dataset, new size
length(train_ds) ~ 2K, and a larger validation
set, new size length(valid_ds)~1K. In practice, we can use
any alternative transfer-learning strategy including
pytorch, tensorflow, theano,
etc.
Note that unet training takes significant computational
time; training 20-epochs took a total of 600 compute hours, which
translates into a couple of days of computing on a 20-core server. We
have provided several
precomputed/pre-trained *.pt models on Canvas.
Below, we define a function brain_dataset() required for
iterative retrieval of pytorch datasets. All such datasets need to have
a method called initialize() instantiating the inventory of
imaging and mask file names that will be used by the
second method, .getitem(), to ingest the imaging data from
these files, return input-image plus mask-target pairs, and perform
data-augmentation. The parameter random_sampling=TRUE,
.getitem() controls the weighted sample loading of
image-mask pairs with larger in size tumors. This option is used with
the training set to counter any class-label imbalances.
The training sets, but not the validation sets, use of data augmentation to ensure DCNN-invariance to specific spatiotemporal and intensity transformations. During imaging-data augmentation, the training images/masks may be flipped, resized, and rotated with specifiable probabilities for each type of augmentation transformation.
# install.packages(torch)
library(torch)
library(torchvision)
# data wrangling
library(tidyverse)
library(zeallot) # needed for the piping function "%<-%" in "brain_dataset()"
# image processing and visualization
library(magick)
#library(cowplot)
# dataset loading
library(pins)
library(zip)
torch_manual_seed(1234)
set.seed(1234)
train_dir <- file.path(getwd(),"data","data")
valid_dir <- file.path(getwd(),"data","mri_valid")
brain_dataset <- dataset(
name = "brain_dataset",
# 1. Initialize
initialize = function(img_dir, augmentation_params = NULL, random_sampling = FALSE) {
self$images <- tibble(img = grep(list.files(img_dir, full.names = TRUE,
pattern = "tif", recursive = TRUE),
pattern = 'mask', invert = TRUE, value = TRUE),
mask = grep(list.files(img_dir, full.names = TRUE,
pattern = "tif", recursive = TRUE),
pattern = 'mask',value = TRUE)
)
self$slice_weights <- self$calc_slice_weights(self$images$mask)
self$augmentation_params <- augmentation_params
self$random_sampling <- random_sampling
},
# 2. Load and transform images from files into TF object elements (tensors)
.getitem = function(i) {index <- if (self$random_sampling == TRUE) sample(1:self$.length(), 1, prob = self$slice_weights)
else i
img <- self$images$img[index] %>% image_read() %>% transform_to_tensor()
mask <- self$images$mask[index] %>% image_read() %>% transform_to_tensor() %>% transform_rgb_to_grayscale() %>%
torch_unsqueeze(1)
img <- self$min_max_scale(img)
if (!is.null(self$augmentation_params)) {
scale_param <- self$augmentation_params[1]
c(img, mask) %<-% self$resize(img, mask, scale_param)
rot_param <- self$augmentation_params[2]
c(img, mask) %<-% self$rotate(img, mask, rot_param)
flip_param <- self$augmentation_params[3]
c(img, mask) %<-% self$flip(img, mask, flip_param)
}
list(img = img, mask = mask)
},
# 3. Save the total number of imaging files
.length = function() { nrow(self$images) },
# 4. Estimate 2D image weights: Bigger tumor-masks correspond to higher weights
calc_slice_weights = function(masks) {
weights <- map_dbl(masks, function(m) {
img <- as.integer(magick::image_data(image_read(m), channels = "gray"))
sum(img / 255)
})
sum_weights <- sum(weights)
num_weights <- length(weights)
weights <- weights %>% map_dbl(function(w) {
w <- (w + sum_weights * 0.1 / num_weights) / (sum_weights * 1.1)
})
weights
},
# 5. Estimate the image intensity range (min, max)
min_max_scale = function(x) {
min = x$min()$item()
max = x$max()$item()
x$clamp_(min = min, max = max)
x$add_(-min)$div_(max - min + 1e-5)
x
},
# 6. Image tensor shape resizing (when necessary)
resize = function(img, mask, scale_param) {
img_size <- dim(img)[2]
rnd_scale <- runif(1, 1 - scale_param, 1 + scale_param)
img <- transform_resize(img, size = rnd_scale * img_size)
mask <- transform_resize(mask, size = rnd_scale * img_size)
diff <- dim(img)[2] - img_size
if (diff > 0) {
top <- ceiling(diff / 2)
left <- ceiling(diff / 2)
img <- transform_crop(img, top, left, img_size, img_size)
mask <- transform_crop(mask, top, left, img_size, img_size)
} else {
img <- transform_pad(img,padding = -c(ceiling(diff/2),floor(diff/2),ceiling(diff/2),floor(diff/2)))
mask <- transform_pad(mask, padding = -c(ceiling(diff/2), floor(diff/2),ceiling(diff/2),floor(diff/2)))
}
list(img, mask)
},
# 7. Rotation (if/when augmentation is requested)
rotate = function(img, mask, rot_param) {
rnd_rot <- runif(1, 1 - rot_param, 1 + rot_param)
img <- transform_rotate(img, angle = rnd_rot)
mask <- transform_rotate(mask, angle = rnd_rot)
list(img, mask)
},
# 8. Flipping (if/when augmentation is requested)
flip = function(img, mask, flip_param) {
rnd_flip <- runif(1)
if (rnd_flip > flip_param) {
img <- transform_hflip(img)
mask <- transform_hflip(mask)
}
list(img, mask)
}
)Next, using the brain_dataset() method, we actually
generate the (training and validation) imaging datasets as computable
objects using the raw filenames in the training and validation
data-frames defined above.
train_ds <- brain_dataset(
train_dir,
augmentation_params = c(0.05, 15, 0.5),
random_sampling = TRUE
)
length(train_ds)## [1] 3317
# ~3K
valid_ds <- brain_dataset(
valid_dir,
augmentation_params = NULL,
random_sampling = FALSE
)
length(valid_ds)## [1] 612
Let’s visualize one testing and one validation image-mask pairs using
plot_ly().
# if (!require("BiocManager", quietly = TRUE))
# install.packages("BiocManager")
#
# BiocManager::install("EBImage")
library(ggplot2)
library(patchwork) # for combining ggplots side‑by‑side
library (plotly)
rasterPlotly <- function (image, name="", hovermode = NULL) {
myPlot <- plot_ly(type="image", z=image, name=name, hoverlabel=name, text=name,
hovertext=name, hoverinfo="name+x+y") %>%
layout(hovermode = hovermode, xaxis = list(hoverformat = '.1f'), yaxis = list(hoverformat = '.1f'))
return(myPlot)
}
## Training Case 20
img_and_mask <- train_ds[20]
img <- img_and_mask[[1]] # 3-channel image
img <- img_and_mask[[1]]$permute(c(2, 3, 1)) %>% as.array() # 3-channel image
mask <- img_and_mask[[2]]$squeeze() %>% as.array() # tumor mask
# mask <- EBImage::rgbImage(255*mask, 255*mask, 255*mask) # manually generate a gray scale mask image
# plot_ly(z=255*bird_images[i+ (j-1)*10,,,], type="image", showscale=FALSE)
# p1 <- rasterPlotly(image=255*img, name = "RGB Image", hovermode = "y unified")
# p2 <- rasterPlotly(mask, name = "Tumor Mask", hovermode = "y unified")
# subplot(p1,p2, shareY = TRUE) %>% layout(title="Training Case 20: 3-Channel Image (Left) & Tumor Mask (Right)")
#
# # Validation Case 18
# img_and_mask <- valid_ds[18]
# img <- img_and_mask[[1]]
# mask <- img_and_mask[[2]]
# img <- img_and_mask[[1]]$permute(c(2, 3, 1)) %>% as.array() # 3-channel image
# mask <- img_and_mask[[2]]$squeeze() %>% as.array() # tumor mask
# mask <- EBImage::rgbImage(255*mask, 255*mask, 255*mask) # manually generate a gray scale mask image
#
# p1 <- rasterPlotly(image=255*img, name = "RGB Image", hovermode = "y unified")
# p2 <- rasterPlotly(mask, name = "Tumor Mask", hovermode = "y unified")
# subplot(p1,p2, shareY = TRUE) %>% layout(title="Validation Case 18: 3-Channel Image (Left) & Tumor Mask (Right)")
# Convert to data frames for ggplot
df_img <- expand.grid(x = 1:ncol(img), y = 1:nrow(img))
df_img$value <- as.vector(img[,,1]) # use first channel for grayscale display
df_img$type <- "RGB Image"
df_mask <- expand.grid(x = 1:ncol(mask), y = 1:nrow(mask))
df_mask$value <- as.vector(mask)
df_mask$type <- "Tumor Mask"
# Combine and plot
bind_rows(df_img, df_mask) %>%
ggplot(aes(x = x, y = y, fill = value)) +
geom_raster() +
facet_wrap(~ type, nrow = 1) +
scale_y_reverse() +
scale_fill_gradient(low = "black", high = "white") +
coord_fixed() +
theme_void() +
theme(legend.position = "none",
strip.text = element_text(size = 12, face = "bold")) +
labs(title = "Training Case 20: 3‑Channel Image (Left) & Tumor Mask (Right)")### Training Case 18
img_and_mask <- valid_ds[18]
img <- img_and_mask[[1]]$permute(c(2,3,1)) %>% as.array()
mask <- img_and_mask[[2]]$squeeze() %>% as.array()
# mask <- EBImage::rgbImage(255*mask, 255*mask, 255*mask) # manually generate a gray scale mask image
df_img <- expand.grid(x = 1:ncol(img), y = 1:nrow(img))
df_img$value <- as.vector(img[,,1])
df_img$type <- "RGB Image"
df_mask <- expand.grid(x = 1:ncol(mask), y = 1:nrow(mask))
df_mask$value <- as.vector(mask)
df_mask$type <- "Tumor Mask"
bind_rows(df_img, df_mask) %>%
ggplot(aes(x = x, y = y, fill = value)) +
geom_raster() +
facet_wrap(~ type, nrow = 1) +
scale_y_reverse() +
scale_fill_gradient(low = "black", high = "white") +
coord_fixed() +
theme_void() +
theme(legend.position = "none",
strip.text = element_text(size = 12, face = "bold")) +
labs(title = "Validation Case 18: 3‑Channel Image (Left) & Tumor Mask (Right)")Demonstrate training-data image-augmentation by using one image to generate 7 rows of 4 augmented images.
# # OLD PLOT_LY version
# N <- 7*4
# img_and_mask <- valid_ds[77]
# img <- img_and_mask[[1]]
# mask <- img_and_mask[[2]]
#
# imgs <- map (1:N, function(i) {
# # spatial-scale factor
# c(img, mask) %<-% train_ds$resize(img, mask, 0.25)
# c(img, mask) %<-% train_ds$flip(img, mask, 0.3)
# c(img, mask) %<-% train_ds$rotate(img, mask, 45)
#
# img %>%
# transform_rgb_to_grayscale() %>%
# as.array() %>%
# plot_ly(z=., type="heatmap", showscale = FALSE, name=paste0("AugmImg=", i)) %>%
# layout(showlegend=FALSE, hovermode = "y unified") #,
# # yaxis = list(scaleratio = 1, scaleanchor = 'x'))
# })
#
# # imgs[[2]]
# imgs %>%
# subplot(nrows = 7, shareX = TRUE, shareY = TRUE, which_layout=1) %>%
# layout(title="Validation Case 77: Random Rotation/Flop/Scale Image Augmentation")
library(ggplot2)
library(dplyr)
library(purrr)
library(tidyr)
N <- 7 * 4
img_and_mask <- valid_ds[77]
img <- img_and_mask[[1]]
mask <- img_and_mask[[2]]
all_imgs <- map_dfr(1:N, function(i) {
c(img, mask) %<-% train_ds$resize(img, mask, 0.25)
c(img, mask) %<-% train_ds$flip(img, mask, 0.3)
c(img, mask) %<-% train_ds$rotate(img, mask, 45)
mat <- img %>%
transform_rgb_to_grayscale() %>%
as.array()
expand.grid(x = 1:ncol(mat), y = 1:nrow(mat)) %>%
mutate(value = as.vector(mat),
aug_id = paste0("AugmImg=", i))
})
ggplot(all_imgs, aes(x = x, y = y, fill = value)) +
geom_raster() +
facet_wrap(~ aug_id, nrow = 7) +
scale_y_reverse() +
scale_fill_gradient(low = "black", high = "white") +
coord_fixed() +
theme_void() +
theme(legend.position = "none",
plot.title = element_text(hjust = 0.5)) +
labs(title = "Validation Case 77: Random Rotation/Flip/Scale Image Augmentation")Instantiate training/validation data loaders using
torch::dataloader(), which combines a data-set and a
sampler-iterator into a single/multi-process iterator over the entire
dataset.
batch_size <- 25
# train_dl <- dataloader(train_ds, batch_size)
# valid_dl <- dataloader(valid_ds, batch_size)
train_dl <- train_ds %>% dataloader(batch_size = batch_size, shuffle = TRUE)
valid_dl <- valid_ds %>% dataloader(batch_size = batch_size, shuffle = FALSE)Next, we specify the Unet model indicating the number of
“down” or encoding (analysis) depth for shrinking the input images and
incrementing the number of filters, as well as specify how do we go “up”
again during the decoding (synthesis) phase.
# forward(), keeps track of layer outputs seen going “down,” to be added back in going “up.”
unet <- nn_module(name="unet",
initialize = function(channels_in = 3, n_classes = 1, depth = 5, n_filters = 6) {
self$down_path <- nn_module_list()
prev_channels <- channels_in
for (i in 1:depth) {
self$down_path$append(down_block(prev_channels, 2^(n_filters+i-1)))
prev_channels <- 2^(n_filters+i-1)
}
self$up_path <- nn_module_list()
for (i in ((depth - 1):1)) {
self$up_path$append(up_block(prev_channels, 2^(n_filters+i-1)))
prev_channels <- 2^(n_filters+i-1)
}
self$last = nn_conv2d(prev_channels, n_classes, kernel_size = 1)
},
forward = function(x) {
blocks <- list()
for (i in 1:length(self$down_path)) {
x <- self$down_path[[i]](x)
if (i != length(self$down_path)) {
blocks <- c(blocks, x)
x <- nnf_max_pool2d(x, 2)
}
}
for (i in 1:length(self$up_path)) {
x <- self$up_path[[i]](x, blocks[[length(blocks)-i+1]]$to(device=device))
}
torch_sigmoid(self$last(x))
}
)
# unet utilizes `down_block` and `up_block`
# down_block delegates to its own workhorse, conv_block, and up_block “bridges” the UNET
down_block <- nn_module(
classname="down_block",
initialize = function(in_size, out_size) {
self$conv_block <- conv_block(in_size, out_size)
},
forward = function(x) { self$conv_block(x) }
)
up_block <- nn_module(
classname="up_block",
initialize = function(in_size, out_size) {
self$up = nn_conv_transpose2d(in_size, out_size, kernel_size=2, stride=2)
self$conv_block = conv_block(in_size, out_size)
},
forward = function(x, bridge) {
up <- self$up(x)
torch_cat(list(up, bridge), 2) %>% self$conv_block()
}
)
conv_block <- nn_module(
classname="conv_block",
initialize = function(in_size, out_size) {
self$conv_block <- nn_sequential(
nn_conv2d(in_size, out_size, kernel_size = 3, padding = 1),
nn_relu(),
nn_dropout(0.6),
nn_conv2d(out_size, out_size, kernel_size = 3, padding = 1),
nn_relu()
)
},
forward = function(x){
self$conv_block(x)
}
)Before we start the unet model training, we need to
specify CPU/GPU device and optimization scheme.
# Initialize the model with appropriate CPU/GPU
device <- torch_device(if(cuda_is_available()) "cuda" else "cpu")
device <-"cpu"
model <- unet(depth = 5)$to(device = device)
# OPTIMIZATION
# DCNN model training using cross_entropy and dice_loss.
calc_dice_loss <- function(y_pred, y_true) {
smooth <- 1
y_pred <- y_pred$view(-1)
y_true <- y_true$view(-1)
intersection <- (y_pred * y_true)$sum()
1 - ((2*intersection+smooth)/(y_pred$sum()+y_true$sum()+smooth))
}
dice_weight <- 0.3
# learning_rate = 0.1
optimizer <- optim_sgd(model$parameters, lr = 0.1, momentum = 0.9)On a multi-core machine, training the UNET model will
require ~2-hrs per epoch. This step can be skipped if you load in a
previously pre-trained model (model) that is saved
(torch_save(model, "/path/model.pt")) and available for
import (torch_load("/path/model.ptt")), see
several *.pt model here.
num_epochs <- 1 # for knitting, otherwise increase to 5+
scheduler <- lr_one_cycle(optimizer, max_lr = 0.1, steps_per_epoch = length(train_dl), epochs = num_epochs)
# TRAINING
train_batch <- function(b) {
optimizer$zero_grad()
output <- model(b[[1]]$to(device = device))
target <- b[[2]]$to(device = device)
bce_loss <- nnf_binary_cross_entropy(output, target)
dice_loss <- calc_dice_loss(output, target)
loss <- dice_weight*dice_loss + (1-dice_weight)*bce_loss
loss$backward()
optimizer$step()
scheduler$step()
list(bce_loss$item(), dice_loss$item(), loss$item())
}
valid_batch <- function(b) {
output <- model(b[[1]]$to(device = device))
target <- b[[2]]$to(device = device)
bce_loss <- nnf_binary_cross_entropy(output, target)
dice_loss <- calc_dice_loss(output, target)
loss <- dice_weight * dice_loss + (1-dice_weight)*bce_loss
list(bce_loss$item(), dice_loss$item(), loss$item())
}Start the Unet training process (this is suspended here
to enable real-time knitting of the HTML doc).
for (epoch in 1:num_epochs) {
model$train()
train_bce <- c()
train_dice <- c()
train_loss <- c()
coro::loop(for (b in train_dl) {
c(bce_loss, dice_loss, loss) %<-% train_batch(b)
train_bce <- c(train_bce, bce_loss)
train_dice <- c(train_dice, dice_loss)
train_loss <- c(train_loss, loss)
})
torch_save(model, paste0(getwd(),"/model_", epoch, ".pt"))
cat(sprintf("\nEpoch %d, training: loss:%3f, bce: %3f, dice: %3f\n",
epoch, mean(train_loss), mean(train_bce), mean(train_dice)))
model$eval()
valid_bce <- c()
valid_dice <- c()
valid_loss <- c()
i <- 0
coro::loop(for (b in valid_dl) {
i <<- i + 1
c(bce_loss, dice_loss, loss) %<-% valid_batch(b)
valid_bce <- c(valid_bce, bce_loss)
valid_dice <- c(valid_dice, dice_loss)
valid_loss <- c(valid_loss, loss)
})
cat(sprintf("\n Epoch %d, validation: loss: %3f, bce: %3f, dice: %3f\n",
epoch, mean(valid_loss), mean(valid_bce), mean(valid_dice)))
}
# note that DCNN model mask prediction will be based on the model complexity!
# Epoch 1, training: loss:0.495716, bce: 0.307674, dice: 0.934480
# Epoch 1, validation: loss: 0.336556, bce: 0.070743, dice: 0.956785
#
# Epoch 2, training: loss:0.290850, bce: 0.114878, dice: 0.701452
# Epoch 2, validation: loss: 0.213417, bce: 0.046690, dice: 0.602447
#
# Epoch 3, training: loss:0.204054, bce: 0.102973, dice: 0.439909
# Epoch 3, validation: loss: 0.218209, bce: 0.051383, dice: 0.607468
#
# Epoch 4, training: loss:0.199831, bce: 0.101564, dice: 0.429123
# Epoch 4, validation: loss: 0.243201, bce: 0.068757, dice: 0.650236
#
# Epoch 5, training: loss:0.201025, bce: 0.101525, dice: 0.433192
# Epoch 5, validation: loss: 0.258278, bce: 0.077821, dice: 0.679345
#
# Epoch 6, training: loss:0.188058, bce: 0.094279, dice: 0.406877
# Epoch 6, validation: loss: 0.206198, bce: 0.038769, dice: 0.596865
#
# Epoch 7, training: loss:0.187285, bce: 0.094894, dice: 0.402866
# Epoch 7, validation: loss: 0.216684, bce: 0.050536, dice: 0.604361
#
# Epoch 8, training: loss:0.184097, bce: 0.093609, dice: 0.395237
# Epoch 8, validation: loss: 0.235675, bce: 0.060247, dice: 0.645006
#
# Epoch 9, training: loss:0.179406, bce: 0.090458, dice: 0.386951
# Epoch 9, validation: loss: 0.238004, bce: 0.055283, dice: 0.664353
#
# Epoch 10, training: loss:0.175083, bce: 0.089144, dice: 0.375608
# Epoch 10, validation: loss: 0.258206, bce: 0.080033, dice: 0.673942
#
# Epoch 11, training: loss:0.175821, bce: 0.089795, dice: 0.376549
# Epoch 11, validation: loss: 0.213895, bce: 0.037936, dice: 0.624464
#
# Epoch 12, training: loss:0.174915, bce: 0.087026, dice: 0.379987
# Epoch 12, validation: loss: 0.264673, bce: 0.083884, dice: 0.686514
#
# Epoch 13, training: loss:0.171973, bce: 0.087210, dice: 0.369754
# Epoch 13, validation: loss: 0.236396, bce: 0.065432, dice: 0.635311
#
# Epoch 14, training: loss:0.167253, bce: 0.084660, dice: 0.359970
# Epoch 14, validation: loss: 0.210641, bce: 0.047418, dice: 0.591495
#
# Epoch 15, training: loss:0.170835, bce: 0.083572, dice: 0.374449
# Epoch 15, validation: loss: 0.246905, bce: 0.067056, dice: 0.666552
#
# Epoch 16, training: loss:0.167938, bce: 0.083285, dice: 0.365460
# Epoch 16, validation: loss: 0.228558, bce: 0.057135, dice: 0.628546
#
# Epoch 17, training: loss:0.161383, bce: 0.080367, dice: 0.350421
# Epoch 17, validation: loss: 0.221012, bce: 0.055709, dice: 0.606719
#
# Epoch 18, training: loss:0.158193, bce: 0.078068, dice: 0.345150
# Epoch 18, validation: loss: 0.222881, bce: 0.052606, dice: 0.620188
#
# Epoch 19, training: loss:0.158190, bce: 0.078865, dice: 0.343280
# Epoch 19, validation: loss: 0.223159, bce: 0.054643, dice: 0.616361
#
# Epoch 20, training: loss:0.161496, bce: 0.079807, dice: 0.352102
# Epoch 20, validation: loss: 0.221487, bce: 0.051793, dice: 0.617438
# > proc.time() # In Seconds! About 2,154,523 sec ~ 598 hours
# user system elapsed
# 2154523.7 776791.3 141279.2Evaluate the trained DCNN model using model_6.pt and a
batch of \(n=10\) validation datasets
(2D brain imaging scans).
# EVALUATION
# without random sampling, we'd mainly see lesion-free patches
###### Old interactive plot_ly() version
# N <- 10 # number of cases to predict the masks for
#
# eval_ds <- brain_dataset(valid_dir, augmentation_params = NULL, random_sampling = TRUE)
# eval_dl <- dataloader(eval_ds, batch_size = N)
#
# batch <- eval_dl %>% dataloader_make_iter() %>% dataloader_next()
#
# # Load a previously save torch model
# # epoch = 1
# # torch_load(model, paste0("model_", epoch, ".pt"))
#
# # torch_save(model, "C:/Users/Dinov/Desktop/model_epoch_1.pt")
#
# ##### Requirements for loading a pre-trained model .....
# # load torch packages above
# # The pre-computed model files are available on Canvas:
# # https://umich.instructure.com/courses/38100/files/folder/Case_Studies/36_TCGA_MRI_Segmentation_Data_Phenotypes
# # localModelsFolder <- "C:/Users/IvoD/Desktop/Ivo.dir/Research/UMichigan/Publications_Books/2023/DSPA_Springer_2nd_Edition_2023/Rmd_HTML/appendix/"
#
# localModelsFolder <-getwd()
#
# model <- torch_load(paste0(localModelsFolder, "/appendix/model_6.pt"))
# # train_dir <- "/data/data"
# # valid_dir <- "/data/mri_valid"
# # brain_dataset ...
# # train_ds; valid_ds
# # eval_ds <- brain_dataset(valid_dir,augmentation_params=NULL,random_sampling=TRUE)
# # eval_dl <- dataloader(eval_ds, batch_size = 8)
# # batch <- eval_dl %>% dataloader_make_iter() %>% dataloader_next()
# # device <- torch_device(if(cuda_is_available()) "cuda" else "cpu")
# # calc_dice_loss
# library(plotly)
#
# imgsTruePred <- map (1:N, function(i) {
# # Get the 3 images
# img <- batch[[1]][i, .., drop = FALSE] # Image to predict the tumor mask for
# inferred_mask <- model(img$to(device = device)) # Predicted MASK
# true_mask <- batch[[2]][i, .., drop = FALSE]$to(device = device) # True manual mask delineation
#
# # compute/report the BCE/Dice performance metrics
# bce <- nnf_binary_cross_entropy(inferred_mask, true_mask)$to(device = "cpu") %>% as.numeric()
# dc <- calc_dice_loss(inferred_mask, true_mask)$to(device = "cpu") %>% as.numeric()
# cat(sprintf("\nSample %d, bce: %3f, dice: %3f\n", i, bce, dc))
#
# # extract the inferred predicted mask as a 2D image/array of probability values per voxel!
# inferred_mask <- inferred_mask$to(device = "cpu") %>% as.array() %>% .[1, 1, , ]
# # Binarize the probability tumor prediction to binary mask
# inferred_mask <- ifelse(inferred_mask > 0.48, 1, 0) # In a real run, use "inferred_mask > 0.5, 1, 0"
# # hist(as.matrix(inferred_mask[1,1,,]))
#
# imgs <- img[1, 1, ,] %>% as.array() %>% as.array() %>%
# plot_ly(z=., type="heatmap", showscale = FALSE, name="Image") %>%
# layout(showlegend=FALSE, yaxis = list(scaleanchor = "x", scaleratio = 1)) # hovermode = "y unified"
# masks <- true_mask$to(device = "cpu")[1, 1, ,] %>% as.array() %>% as.array() %>%
# plot_ly(z=., type="heatmap", showscale = FALSE, name="True Mask") %>%
# layout(showlegend=FALSE, yaxis = list(scaleanchor = "x", scaleratio = 1)) # hovermode = "y unified",
# predMasks <- inferred_mask %>% as.array() %>%
# plot_ly(z=., type="heatmap", showscale = FALSE, name="Unet-Derived Mask") %>%
# layout(showlegend=FALSE, yaxis = list(scaleanchor = "x", scaleratio = 1)) # hovermode = "y unified",
# rowSubPlots <- subplot(imgs, masks, predMasks, nrows = 1, shareY = TRUE) %>%
# # , widths = c(0.3, 0.3, 0.3))
# layout(hovermode = "y unified") #, yaxis = list(scaleanchor = "x", scaleratio = 1))
# })
library(ggplot2)
library(dplyr)
library(purrr)
library(tidyr)
# Evaluate on N validation cases (same as original)
N <- 10
eval_ds <- brain_dataset(valid_dir, augmentation_params = NULL, random_sampling = TRUE)
eval_dl <- dataloader(eval_ds, batch_size = N)
batch <- eval_dl %>% dataloader_make_iter() %>% dataloader_next()
# Load pre-trained torch model (adjust path as needed)
model <- torch_load(paste0(getwd(), "/appendix/model_6.pt"))
# We'll collect all plots into one data frame
all_cases <- map_dfr(1:N, function(i) {
# Get image, true mask, predicted mask
img_tensor <- batch[[1]][i, .., drop = FALSE] # shape (1,3,256,256) or similar
true_mask_tensor <- batch[[2]][i, .., drop = FALSE]
# Predict
inferred_mask_tensor <- model(img_tensor$to(device = device))
# Compute metrics (optional, but keep for console output)
bce <- nnf_binary_cross_entropy(inferred_mask_tensor, true_mask_tensor)$to(device = "cpu") %>% as.numeric()
dc <- calc_dice_loss(inferred_mask_tensor, true_mask_tensor)$to(device = "cpu") %>% as.numeric()
cat(sprintf("\nSample %d, bce: %3f, dice: %3f\n", i, bce, dc))
# Convert to 2D matrices (use first channel, first batch dimension)
img_mat <- img_tensor$to(device = "cpu")[1, 1, , ] %>% as.array() # choose one channel for display
true_mat <- true_mask_tensor$to(device = "cpu")[1, 1, , ] %>% as.array()
# Predicted mask: probability -> binary
prob_mat <- inferred_mask_tensor$to(device = "cpu") %>% as.array() %>% .[1, 1, , ]
pred_mat <- ifelse(prob_mat > 0.48, 1, 0)
# Build data frames for each type
bind_rows(
expand.grid(x = 1:ncol(img_mat), y = 1:nrow(img_mat)) %>%
mutate(value = as.vector(img_mat),
case = paste0("Case ", i),
type = "Brain Image"),
expand.grid(x = 1:ncol(true_mat), y = 1:nrow(true_mat)) %>%
mutate(value = as.vector(true_mat),
case = paste0("Case ", i),
type = "True Mask"),
expand.grid(x = 1:ncol(pred_mat), y = 1:nrow(pred_mat)) %>%
mutate(value = as.vector(pred_mat),
case = paste0("Case ", i),
type = "Predicted Mask")
)
})##
## Sample 1, bce: 0.316758, dice: 0.771990
##
## Sample 2, bce: 0.057257, dice: 0.983731
##
## Sample 3, bce: 0.033557, dice: 0.126262
##
## Sample 4, bce: 0.031052, dice: 0.566501
##
## Sample 5, bce: 0.085775, dice: 0.217008
##
## Sample 6, bce: 0.086270, dice: 0.215835
##
## Sample 7, bce: 0.197902, dice: 0.480702
##
## Sample 8, bce: 0.032555, dice: 0.177114
##
## Sample 9, bce: 0.157686, dice: 0.461385
##
## Sample 10, bce: 0.189908, dice: 0.350955
# Create static ggplot grid
ggplot(all_cases, aes(x = x, y = y, fill = value)) +
geom_raster() +
facet_grid(case ~ type) + # rows = cases, columns = image types
scale_y_reverse() +
scale_fill_gradient(low = "black", high = "white") +
coord_fixed() +
theme_void() +
theme(legend.position = "none",
strip.text = element_text(size = 10, face = "bold"),
strip.background = element_blank()) +
labs(title = paste0("Model Validation (Torch) – ", N, " Cases\nBrain Image | True Mask | Predicted Mask"))Finally, we can assess the unet model performance on the
independent validation images, report the DCNN-derived tumor masks (as
images) and some quantitative measures (e.g., Binary Cross-Entropy, Dice
coefficient). The figure displays the results - brain image, true mask,
and DCNN-estimated mask.
This image-normalization (standardization) is necessary to avoid extreme differences in image intensities (e.g., sMRI, fMRI, PET, SPECT, dMDI/DTI, MRS, and other imaging modalities tend to have vastly different scales).
h_dim <- c(32, 64, 128, 256, 512)
net_encoder <- nn_module(
initialize = function(latent_size=1000){
self$latent_size <- latent_size # Z
# h_dim <- c(32, 64, 128, 256, 512)
in_channels = 3
# Encoder
self$encoder <- nn_sequential(
nn_conv2d(in_channels, out_channels=h_dim[1],
kernel_size= 3, stride= 2, padding = 1),
nn_batch_norm2d(h_dim[1]),
nn_leaky_relu(),
nn_conv2d(h_dim[1], out_channels=h_dim[2],
kernel_size= 3, stride= 2, padding = 1),
nn_batch_norm2d(h_dim[2]),
nn_leaky_relu(),
nn_conv2d(h_dim[2], out_channels=h_dim[3],
kernel_size= 3, stride= 2, padding = 1),
nn_batch_norm2d(h_dim[3]),
nn_leaky_relu(),
nn_conv2d(h_dim[3], out_channels=h_dim[4],
kernel_size= 3, stride= 2, padding = 1),
nn_batch_norm2d(h_dim[4]),
nn_leaky_relu(),
nn_conv2d(h_dim[4], out_channels=h_dim[5],
kernel_size= 3, stride= 2, padding = 1),
nn_batch_norm2d(h_dim[5]),
nn_leaky_relu()
)
self$mu_layer <- nn_linear(h_dim[5]*64, latent_size)
self$logvar_layer <- nn_linear(h_dim[5]*64, latent_size)
},
forward = function(x){
list_output = c()
hidden = self$encoder(x)
hidden = torch_flatten(hidden, start_dim=2)
mu = self$mu_layer(hidden)
logvar = self$logvar_layer(hidden)
z = reparametrize(mu, logvar)
list_output = append(list_output, z)
list_output = append(list_output, mu)
list_output = append(list_output, logvar)
return (list_output)
}
)net_decoder <- nn_module(
initialize = function(latent_size=1000){
self$latent_size <- latent_size # Z
self$decoder_input <- nn_linear(latent_size, h_dim[5]*64)
hidden_dims = c(32, 64, 128, 256, 512)
# decoder
self$decoder <- nn_sequential(
nn_conv_transpose2d(hidden_dims[5],
hidden_dims[4],
kernel_size=3,
stride = 2,
padding=1,
output_padding=1),
nn_batch_norm2d(hidden_dims[4]),
nn_leaky_relu(),
nn_conv_transpose2d(hidden_dims[4],
hidden_dims[3],
kernel_size=3,
stride = 2,
padding=1,
output_padding=1),
nn_batch_norm2d(hidden_dims[3]),
nn_leaky_relu(),
nn_conv_transpose2d(hidden_dims[3],
hidden_dims[2],
kernel_size=3,
stride = 2,
padding=1,
output_padding=1),
nn_batch_norm2d(hidden_dims[2]),
nn_leaky_relu(),
nn_conv_transpose2d(hidden_dims[2],
hidden_dims[1],
kernel_size=3,
stride = 2,
padding=1,
output_padding=1),
nn_batch_norm2d(hidden_dims[1]),
nn_leaky_relu()
)
self$final_layer <- nn_sequential(
nn_conv_transpose2d(hidden_dims[1],
hidden_dims[1],
kernel_size=3,
stride=2,
padding=1,
output_padding=1),
nn_batch_norm2d(hidden_dims[1]),
nn_leaky_relu(),
nn_conv2d(hidden_dims[1], out_channels= 3,
kernel_size= 3, padding= 1),
nn_sigmoid())
},
forward = function(z){
z = self$decoder_input(z)
z = z$view(c(-1, 512, 8, 8))
x_hat = self$final_layer(self$decoder(z))
return (x_hat)
}
)For GPU optimization of this long calculation see this GPUComputingWithR module.
model_encoder = net_encoder()
model_decoder = net_decoder()
# model = model$to(device='cuda')
optimizer <- optim_adam(model_decoder$parameters)
# optimizer <- optim_adam(model$parameters)
# train_data <- train_data / 255 # torch tensor with shape (N, C, H, W)
# torch_model$cuda()
# Epochs
capture.output( # Capture/Suppress Pytorch output, which overwhelms the knitted HTML report
for (epoch in 1:5) { # could increase the number of epochs for better results
l <- c()
coro::loop(for (b in train_dl) {
optimizer$zero_grad()
gnd = b[[1]]
list_output <- model_encoder(gnd)
z = list_output[[1]]
mu = list_output[[2]]
logvar = list_output[[3]]
output <- model_decoder(z)
loss <- loss_function(output, gnd, mu, logvar)
print(loss)
loss$backward()
optimizer$step()
l <- c(l, loss$item())
})
cat(sprintf("Loss at epoch %d: %3f\n", epoch, mean(l)))
})
# capture.output( # Capture/Suppress Pytorch output, which overwhelms the knitted HTML report
# for (epoch in 1:5) { # could increase the number of epochs for better results
# l <- c()
# coro::loop(for (b in train_dl) {
# # optimizer$zero_grad()
# encoderOptimizer$zero_grad()
# decoderOptimizer$zero_grad()
# gnd = b[[1]]
# list_output <- model_encoder(gnd)
# z = list_output[[1]]
# mu = list_output[[2]]
# logvar = list_output[[3]]
# output <- model_decoder(z)
#
# loss <- loss_function(output, gnd, mu, logvar)
# print(loss)
# loss$backward()
# # optimizer$step()
# encoderOptimizer$step()
# decoderOptimizer$step()
# l <- c(l, loss$item())
# })
#
# cat(sprintf("Loss at epoch %d: %3f\n", epoch, mean(l)))
# })The synthetic images are derived by randomly sampling the latent space of the DNN (bottom of network) and using solely the decoding branch of the UNet.
img <- (train_ds[1][1]$img)$view(c(1, 3, 256, 256))
z = (model_encoder(img))[[1]]
reconstructed <- model_decoder(z)
reconstructed <- (reconstructed$view(c(3, 256, 256)))$permute(c(2, 3, 1)) %>% as.array()
# Generate a random tensor of numbers in the Latent space using a uniform distribution on the interval [0,1)
# these will be used to seed the VAE decoder branch of the Unet and synthetically generate 2D brain images
z <- torch_rand(c(1, 1000))
generated_images <- model_decoder(z)
generated_images <- (generated_images$view(c(3, 256, 256)))$permute(c(2, 3, 1)) %>% as.array()Finally, plot examples of the DNN-derived image reconstruction and synthetic image generation.
library (plotly)
rasterPlotly <- function (image, name="", hovermode = NULL) {
myPlot <- plot_ly(type="image", z=image, name=name, hoverlabel=name, text=name,
hovertext=name, hoverinfo="name+x+y+z") %>%
layout(title=name, hovermode = hovermode, xaxis = list(hoverformat = '.1f'), yaxis = list(hoverformat = '.1f'))
return(myPlot)
}
p1 <- rasterPlotly(image=255*reconstructed, name = "DNN-reconstructed RGB Image")
# rasterPlotly(image=255*generated_images, name = "Synthetic DNN-generated RGB Image")
p2 <- rasterPlotly(image=20/(0.1+generated_images), name = "Synthetic DNN-generated RGB Image")
# subplot(p1, p2) %>% layout(title="DNN (Unet) Image Reconstruction and Synthetic Generation")
p1The image prepossessing steps above use torch syntax. This blog provides
context on similarities and differences between
torch/pytorch/libtorch and
tensorflow/keras.
Next, we will use tensorflow (TF) to again
ingest independently the images employing tf$image
functions, e.g., decode_png(). This introduction
to RStudio-Tensorflow is helpful to understand the tf
syntax.
Assuming the data is already loaded, see the previous section
(Torch Data Import), we will first preload all the
necessary tensorflow libraries.
## used (Mb) gc trigger (Mb) max used (Mb)
## Ncells 3534060 188.8 7236325 386.5 7236325 386.5
## Vcells 16495443 125.9 53993932 412.0 84365517 643.7
venv_name <- "r-tensorflow"
reticulate::use_virtualenv(virtualenv = venv_name, required = TRUE)
library(reticulate)
library(tensorflow)
library(tfdatasets)
library(rsample) # for training() method
# library(reticulate)
library(purrr)
library(keras)
library(unet)
library(tibble)
library(plotly)Remember that we already have the ZIP data (training=data
and validation=mri_valid) downloaded and expanded in a local
partition /data/. We will split the data into
training:testing \(80:20\) and read the
imaging/masking data from the PNG files into tensorflow data objects
training_dataset and testing_dataset (this is
the separate validation set).
library(rsample)
#### OLD
# train_dir <- "/data/data"
# valid_dir <- "/data/mri_valid"
#
# # Load PNG images
# data_train <- tibble(
# img = grep(list.files(train_dir, full.names = TRUE, pattern = "\\.png$", recursive = TRUE), # or "tif"
# pattern = 'mask', invert = TRUE, value = TRUE),
# mask = grep(list.files(train_dir, full.names = TRUE, pattern = "\\.png$", recursive = TRUE),
# pattern = 'mask', value = TRUE)
# )
# length(data_train$mask) # [1] 2520
# data_train$img[[1]] # [1] "/data/data/TCGA_CS_4941_19960909/TCGA_CS_4941_19960909_1.tif"
# Use relative paths (assuming the data folders are inside the project's 'data/' directory)
train_dir <- "./data/data"
valid_dir <- "./data/mri_valid"
# Check that directories exist
stopifnot(dir.exists(train_dir), dir.exists(valid_dir))
# Use the correct file extension: .tif (not .png)
pattern <- "\\.png$"
data_train <- tibble(
img = grep(list.files(train_dir, full.names = TRUE, pattern = pattern, recursive = TRUE),
pattern = 'mask', invert = TRUE, value = TRUE),
mask = grep(list.files(train_dir, full.names = TRUE, pattern = pattern, recursive = TRUE),
pattern = 'mask', value = TRUE)
)
data_valid <- tibble(
img = grep(list.files(valid_dir, full.names = TRUE, pattern = pattern, recursive = TRUE),
pattern = 'mask', invert = TRUE, value = TRUE),
mask = grep(list.files(valid_dir, full.names = TRUE, pattern = pattern, recursive = TRUE),
pattern = 'mask', value = TRUE)
)
# Confirm non‑empty data frames
if(nrow(data_train) == 0) stop("No training images found. Check directory and file pattern.")
if(nrow(data_valid) == 0) stop("No validation images found.")
data_train$img[[2520]]; data_train$mask[[2520]]## [1] "./data/data/TCGA_HT_7605_19950916/TCGA_HT_7605_19950916_20.png"
## [1] "./data/data/TCGA_HT_7605_19950916/TCGA_HT_7605_19950916_20_mask.png"
# ##### Convert all TIFF files to PNG (recommended for simplicity)
# ##### Run this once before building the TensorFlow datasets:
# library(magick)
#
# convert_tiff_to_png <- function(dir_path) {
# tiff_files <- list.files(dir_path, pattern = "\\.tif$", full.names = TRUE, recursive = TRUE)
# for (f in tiff_files) {
# img <- image_read(f)
# png_path <- sub("\\.tif$", ".png", f)
# image_write(img, path = png_path, format = "png")
# # Optional: delete original TIFF to save space
# # unlink(f)
# }
# }
#
# convert_tiff_to_png(train_dir)
# convert_tiff_to_png(valid_dir)
# # Or Load the TIFF images
# data_train <- tibble(
# img = grep(list.files(train_dir, full.names = TRUE, pattern = "*\\d\\.tif$", recursive = TRUE),
# pattern = 'mask', invert = TRUE, value = TRUE),
# mask = grep(list.files(train_dir, full.names = TRUE, pattern = "*mask\\.tif$", recursive = TRUE),
# pattern = 'mask', value = TRUE)
# )
# length(data_train$mask) # [1] 3216
# data_train$img[[1]] # [1] "/data/data/TCGA_CS_4941_19960909/TCGA_CS_4941_19960909_1.tif"
# data_train$img[[3216]]; data_train$mask[[3216]]
data_train_valid <- initial_split(data_train, prop = 0.8)
# data_train_valid$data$img[1111]; data_train_valid$data$mask[1111]
# # "/data/data/TCGA_DU_7299_19910417/TCGA_DU_7299_19910417_17_mask.png"
# tarfile <- 'C:/Users/Dinov/Desktop/BrainTumorImagingDataZipArchive.tgz'
# tar(tarfile,'/data/',compression='gzip')
# untar(tarfile,'/data/',compression='gzip')
# Check the tensor shape
# tf$image$decode_png(tf$io$read_file(data_train$img[[1]]))
# Inspect the structure of data_train_valid
# str(data_train_valid)
training_dataset <- training(data_train_valid) %>%
tensor_slices_dataset() %>%
dataset_map(~.x %>% list_modify(
# decode_jpeg yields a 3d tensor of shape (256, 256, 3)
# Check tensor shapes!
img = tf$image$decode_png(tf$io$read_file(.x$img), channels = 3),
# img = ifelse(as.character(tf$image$decode_png(tf$io$read_file(.x$img)))$shape=="(256, 256, 1)",
# fix3ChannelImg(tf$image$decode_png(tf$io$read_file(.x$img))),
# tf$image$decode_png(tf$io$read_file(.x$img))),
# Note that decode_gif yields a 4d tensor of shape (1, 256, 256, 3),
# so we remove the unneeded batch dimension and all but one
# of the 3 (identical) channels: tf$image$decode_gif(tf$io$read_file(.x$mask))[1,,,][,,1,drop=FALSE]
mask = tf$image$decode_png(tf$io$read_file(.x$mask))
# wrong_tensor_shape = (as.character(.x$shape) =="(256, 256, 1)")
# if (as.character(img$shape) == "(256, 256, 1)")
# img = tf$image$grayscale_to_rgb(img)
))
# %>%
# tfdatasets::dataset_filter(
# function(record) {
# tf$equal(as.character(record$img$shape), "(256, 256, 3)")
# }
# )
testing_dataset <- testing(data_train_valid) %>%
tensor_slices_dataset() %>%
dataset_map(~.x %>% list_modify(
# decode_jpeg yields a 3d tensor of shape (256, 256, 3)
# Check tensor shapes!
img = tf$image$decode_png(tf$io$read_file(.x$img), channels = 3),
# img = ifelse(as.character(tf$image$decode_png(tf$io$read_file(.x$img)))$shape=="(256, 256, 1)",
# fix3ChannelImg(tf$image$decode_png(tf$io$read_file(.x$img))),
# tf$image$decode_png(tf$io$read_file(.x$img))),
# Note that decode_gif yields a 4d tensor of shape (1, 256, 256, 3),
# so we remove the unneeded batch dimension and all but one
# of the 3 (identical) channels: tf$image$decode_gif(tf$io$read_file(.x$mask))[1,,,][,,1,drop=FALSE]
mask = tf$image$decode_png(tf$io$read_file(.x$mask))
))
# Check the size of the entire (training & testing) TF datasets
tf$data$experimental$cardinality(training_dataset)$numpy() # [1] 2572## [1] 2653
## [1] 664
## $img
## tf.Tensor(
## [[[0 0 0]
## [0 0 0]
## [0 0 0]
## ...
## [0 0 0]
## [0 0 0]
## [0 0 0]]
##
## [[0 0 0]
## [1 0 0]
## [1 0 0]
## ...
## [0 0 0]
## [0 0 0]
## [0 0 0]]
##
## [[0 0 0]
## [0 0 0]
## [0 0 0]
## ...
## [0 0 0]
## [0 0 0]
## [0 0 0]]
##
## ...
##
## [[0 0 0]
## [0 0 0]
## [0 0 0]
## ...
## [0 0 0]
## [0 0 0]
## [0 0 0]]
##
## [[0 0 0]
## [0 0 0]
## [0 0 0]
## ...
## [0 0 0]
## [0 0 0]
## [0 0 0]]
##
## [[0 0 0]
## [0 0 0]
## [0 0 0]
## ...
## [0 0 0]
## [0 0 0]
## [0 0 0]]], shape=(256, 256, 3), dtype=uint8)
##
## $mask
## tf.Tensor(
## [[[0]
## [0]
## [0]
## ...
## [0]
## [0]
## [0]]
##
## [[0]
## [0]
## [0]
## ...
## [0]
## [0]
## [0]]
##
## [[0]
## [0]
## [0]
## ...
## [0]
## [0]
## [0]]
##
## ...
##
## [[0]
## [0]
## [0]
## ...
## [0]
## [0]
## [0]]
##
## [[0]
## [0]
## [0]
## ...
## [0]
## [0]
## [0]]
##
## [[0]
## [0]
## [0]
## ...
## [0]
## [0]
## [0]]], shape=(256, 256, 1), dtype=uint8)
# Check to confirm all brain-images (tensors) are of the same RGB 3-channel shape (256, 256, 3)
capture.output(
tryCatch({
ind2 <- list(); i <- 1
iter <- make_iterator_one_shot(training_dataset)
until_out_of_range({
case <- iterator_get_next(iter)
ind2[[i]] <- ifelse (as.character(case[[1]]$shape)=="(256, 256, 1)", "Incorrect", "Correct")
if (ind2[[i]] =="Incorrect") print(".")
i <- i+1
# print("."); # str(case)
})
print("Check if any brain-images (tensors) are NOT of the correct RGB 3-channel shape (256, 256, 3)")
table(unlist(ind2))
}, error = function(e) e, finally = print("Done!")))## [1] "[1] \"Check if any brain-images (tensors) are NOT of the correct RGB 3-channel shape (256, 256, 3)\""
## [2] "[1] \"Done!\""
## [3] ""
## [4] "Correct "
## [5] " 2653 "
In practice, the uint8 data type of the RGB values in PNG/TIFF files are human-interpretable, the U-net expects floating point tensor elements. Thus, we will convert the 3-channel input images to real numbers and scale them to values in the interval \([0,1)\).
For improving computational efficiency, sometimes it may be necessary to reduce the computational burden by down-sizing the images, e.g., to \(128\times 128\) or even \(32\times 32\). In principle, we want to protect the native aspect ratio in the images, keep the pixels isotropic, and avoid distortion. In this case we won’t reduce the image size, but it may be useful sometimes.
The Tensorflow protocol also includes augmentation of the imaging
data. Earlier, we used torch augmentation, now we use
tensorflow. Of course, we will apply the same
augmentation-transformations (scale, rotate, flip) to the tumor-mask as
well as the brain images, see these three methods resize(),
flip(), rotate() in the
create_dataset() function above. In addition, we can
augment the data by intensity-transformations that preserve the spatial
image structure but alter the contrast, brightness, and image
saturation, see this random_bsh() method.
# mat_to_df helper function used to transform matrix tensors to dataframes
mat_to_df <- function(mat, name) {
expand.grid(x = 1:ncol(mat), y = 1:nrow(mat)) %>%
mutate(value = as.vector(mat),
panel = name)
}
# For data augmentation - contrast, brightness & saturation perturbations
random_bsh <- function(img) {
img %>%
tf$image$random_brightness(max_delta = 0.2) %>%
tf$image$random_contrast(lower = 0.3, upper = 0.6) %>%
tf$image$random_saturation(lower = 0.5, upper = 0.6) %>%
# ensure image intensities are between 0 and 1
tf$clip_by_value(0, 1)
}
# Test Brightness-Saturation-Contrast Hue intensity augmentation
myIterator <- training_dataset %>% reticulate::as_iterator()
#### Old plot_ly() version for interactive testing
# example <- myIterator$get_next(); example <- myIterator$get_next(); example <- myIterator$get_next() # example
# bshExample <- random_bsh(tf$image$convert_image_dtype(example$img, dtype = tf$float32))
#
# arr1 <- array(as.numeric(unlist(example$img)), dim=c(256, 256, 3))
# p1 <- plot_ly(type="heatmap", z=~arr1[,,1], name="Image")
# arr2 <- array(as.numeric(unlist(example$mask)), dim=c(256, 256, 1))
# p2 <- plot_ly(type="heatmap", z=~arr2[,,1], name="Mask")
# arr3 <- 255*array(as.numeric(unlist(bshExample)), dim=c(256, 256, 3))
# p3 <- plot_ly(type="heatmap", z=~arr3[,,1], name="BSC-Image")
# subplot(p1, p2, p3, nrows=1) %>% layout(title="Brightness-Saturation-Contrast Hue Image Intensity Augmentation") %>% hide_colorbar()
library(ggplot2)
library(patchwork)
# Get one example (same as original)
myIterator <- training_dataset %>% reticulate::as_iterator()
example <- myIterator$get_next()
example <- myIterator$get_next()
example <- myIterator$get_next()
bshExample <- random_bsh(tf$image$convert_image_dtype(example$img, dtype = tf$float32))
# Extract matrices
img_mat <- array(as.numeric(unlist(example$img)), dim=c(256,256,3))[,,1] # first channel
mask_mat <- array(as.numeric(unlist(example$mask)), dim=c(256,256,1))[,,1]
bsh_mat <- array(as.numeric(unlist(bshExample)), dim=c(256,256,3))[,,1]
# # Function to convert matrix to data frame for ggplot
# mat_to_df <- function(mat, name) {
# expand.grid(x = 1:ncol(mat), y = 1:nrow(mat)) %>%
# mutate(value = as.vector(mat),
# panel = name)
# }
bind_rows(
mat_to_df(img_mat, "Raw Image"),
mat_to_df(mask_mat, "Raw Mask"),
mat_to_df(bsh_mat, "BSC Image")
) %>%
ggplot(aes(x = x, y = y, fill = value)) +
geom_raster() +
facet_wrap(~ panel, nrow = 1) +
scale_y_reverse() +
scale_fill_gradient(low = "black", high = "white") +
coord_fixed() +
theme_void() +
theme(legend.position = "none",
strip.text = element_text(size = 12, face = "bold")) +
labs(title = "Brightness‑Saturation‑Contrast Hue Image Intensity Augmentation")# Random Rotation
# Rotation requires TF-Addon package: https://www.tensorflow.org/addons/overview
# reticulate::py_install(c('tensorflow-addons'), pip = TRUE)
# tfaddons::install_tfaddons()
# devtools::install_github('henry090/tfaddons')
library(tfaddons)
# random_rotate = function(img, mask, rot_param=15) {
# rnd_rot <- runif(1, -rot_param, rot_param) # random rotation in degrees +/- rot_param
# img <- img_rotate(img, angle = rnd_rot) # tfa$image$rotate(img, angle = rnd_rot)
# mask <- img_rotate(mask, angle = rnd_rot) # tfa$image$rotate(mask, angle = rnd_rot)
# # c(img, mask)
# return (list(img=img, mask=mask))
# }
#### This is a work-around for the data-augmentation rotation problem
#### related to the zeallot right operator `%->%` and the `magrittr` pipe operator `%>%`
rot_param <- 15 # degrees of rotation
rnd_rot <- runif(1000, -rot_param, rot_param)
randomRotationPaired <- c(rbind(rnd_rot, rnd_rot)) # double the random rotations
currentRotationIndex <- 1
random_rotate = function(img, mask) {
img <- img_rotate(img, angle=randomRotationPaired[currentRotationIndex]) # tfa$image$rotate(img, angle = rnd_rot)
mask <- img_rotate(mask, angle=randomRotationPaired[currentRotationIndex]) # tfa$image$rotate(mask, angle = rnd_rot)
# c(img, mask)
currentRotationIndex <- (currentRotationIndex + 1) %% 1000
return (list(img=img, mask=mask))
}
# tesingRot = img_rotate(example$img, angle=10)
# Test rotator
myIterator <- training_dataset %>% reticulate::as_iterator()
example <- myIterator$get_next(); example <- myIterator$get_next(); example <- myIterator$get_next() # example
# arr1 <- array(as.numeric(unlist(example$img)), dim=c(256, 256, 3))
# p1 <- plot_ly(type="heatmap", z=~arr1[,,1], name="Raw Img")
# arr2 <- array(as.numeric(unlist(example$mask)), dim=c(256, 1, 256))
# p2 <- plot_ly(type="heatmap", z=~arr2[,1,], name="Raw Mask")
# subplot(p1, p2, nrows=1) %>% layout(title="Original Image + Mask") %>% hide_colorbar()
#
# rotExample <- random_rotate(example$img, example$mask)
# rotExample$img$shape; rotExample$mask$shape
# arr1 <- array(as.numeric(unlist(rotExample$img)), dim=c(256, 256, 3))
# p1 <- plot_ly(type="heatmap", z=~arr1[,, 1], name="Rotated Img")
# arr2 <- array(as.numeric(unlist(rotExample$mask)), dim=c(256, 256, 1))
# p2 <- plot_ly(type="heatmap", z=~arr2[,, 1], name="Rotated Mask")
# subplot(p1, p2, nrows=1) %>% layout(title="Augmented/Rotated Image + Mask") %>% hide_colorbar()
# Same extraction as above (assuming `example` is still the current batch item)
img_mat_orig <- array(as.numeric(unlist(example$img)), dim=c(256,256,3))[,,1]
mask_mat_orig <- array(as.numeric(unlist(example$mask)), dim=c(256,1,256))[,1,] # shape correction
bind_rows(
mat_to_df(img_mat_orig, "Raw Image"),
mat_to_df(mask_mat_orig, "Raw Mask")
) %>%
ggplot(aes(x = x, y = y, fill = value)) +
geom_raster() +
facet_wrap(~ panel, nrow = 1) +
scale_y_reverse() +
scale_fill_gradient(low = "black", high = "white") +
coord_fixed() +
theme_void() +
theme(legend.position = "none") +
labs(title = "Original Image + Mask")currentRotationIndex <- 1 # reset the currentRotationIndex
# c(img, mask) %<-% random_rotate(example$img, example$mask)
# img$shape; mask$shape
# arr1 <- array(as.numeric(unlist(img)), dim=c(256, 256, 3))
# p1 <- plot_ly(type="heatmap", z=~arr1[,, 1])
# arr2 <- array(as.numeric(unlist(mask)), dim=c(256, 256, 1))
# p2 <- plot_ly(type="heatmap", z=~arr2[,, 1])
# subplot(p1, p2, nrows=1) %>% layout(title="Augmented/Rotated Image + Mask") %>% hide_colorbar()
# Inspect one case
# example <- training_dataset %>% reticulate::as_iterator() %>% reticulate::iter_next()
# example$img %>% as.array() %>% as.raster() %>% plot()The core functions we built above can be integrated into a new
function, create_dataset(), which represents the complete
end-to-end image preprocessing pipeline, The input to this
pipeline is a dataframe of filenames containing the brain-images and
tumor-masks, and the corresponding output contains the
training_dataset and the validation_dataset as
tensorflow dataset-objects that will be used in the model-fitting
phase.
library(zeallot) # for the reverse-piping assignment operator: %<-%
create_dataset <- function(data, train, batch_size = 32L) {
dataset <- data %>% # load all PNG files as images
tensor_slices_dataset() %>%
dataset_map(~.x %>% list_modify(
img = tf$image$decode_png(tf$io$read_file(.x$img), channels = 3),
mask = tf$image$decode_png(tf$io$read_file(.x$mask))
)) %>% # convert image intensities from int8 to 32-bit float
dataset_map(~.x %>% list_modify(
img = tf$image$convert_image_dtype(.x$img, dtype = tf$float32),
mask = tf$image$convert_image_dtype(.x$mask, dtype = tf$float32)
)) %>% # reshape all tensor-shape to (256 * 256) to ensure spatial-index homologies
dataset_map(~.x %>% list_modify(
img = tf$image$resize(.x$img, size = shape(256, 256)),
mask = tf$image$resize(.x$mask, size = shape(256, 256))
))
# data augmentation performed on training set only
if (train) {
dataset <- dataset %>%
dataset_map(~.x %>% list_modify(
# random_rotate(img=.x$img, mask=.x$mask) %->% c(img=img, mask=mask)
# c(img=img, mask=mask) %<-% random_rotate(img=.x$img, mask=.x$mask) # <- Check this issue, it should work like this:
# example <- myIterator$get_next(); example <- myIterator$get_next(); example <- myIterator$get_next()
# c(img, mask) %<-% random_rotate(example[[1]], example[[2]])
# random_rotate(img=example[[1]], mask=example[[2]]) %->% c(img, mask)
# img$shape; mask$shape
#
# Alternative Solutions
# c(img, mask) %<-% random_rotate(.x)
# Syntax: ListOfLists <- c(a, b) %<-% list(a=list(1,2,3), b=list(4,5,6,7)); ListOfLists$b
# c(img, mask) %<-% random_rotate(.x$img, .x$mask)
#
# This works, but we need joint, not separate, rotation of img + mask
img = random_rotate(img=.x$img, mask=.x$mask)$img,
mask= random_rotate(img=.x$img, mask=.x$mask)$mask
# c(img, mask) %<-% unlist(random_rotate(img=.x$img, mask=.x$mask))
# # mask = random_rotate(.x$img, .x$mask)[[2]]
)) %>%
dataset_map(~.x %>% list_modify(
img = random_bsh(.x$img)
))
}
# shuffling on training set only
if (train) {
dataset <- dataset %>%
dataset_shuffle(buffer_size = batch_size*4)
}
# train in batches; batch size might need to be adapted depending on
# available memory
dataset <- dataset %>%
dataset_batch(batch_size)
dataset %>%
# output needs to be unnamed as required by Keras
dataset_map(unname) # makes example$img -> example[[1]]
}
# Generate the Training and Testing data at iterated TF-Data objects
training_dataset <- create_dataset(training(data_train_valid), train = TRUE)
validation_dataset <- create_dataset(testing(data_train_valid), train = FALSE)
# tf$data$experimental$cardinality(training_dataset)$numpy()
myIterator <- training_dataset %>% reticulate::as_iterator()
example <- myIterator$get_next() # ; example <- myIterator$get_next() # shape=(32, 256, 256, 3 or 1)
c(img,mask) %<-% random_rotate(img=example[[1]][1,,,1], mask=example[[2]][1,,,1])
img$shape; mask$shape## TensorShape([256, 256])
## TensorShape([256, 256])
#### Old plot_ly() for interactive demos
# arr1 <- array(as.numeric(img), dim=c(256, 256))
# p1 <- plot_ly(type="heatmap", z=~arr1, name="Rotated Img")
# arr2 <- array(as.numeric(mask), dim=c(256, 256))
# p2 <- plot_ly(type="heatmap", z=~arr2, name="Rotated Mask")
# subplot(p1, p2, nrows=1) %>% layout(title="Augmented/Rotated Image + Mask") %>% hide_colorbar()
# rotExample <- random_rotate(example$img, example$mask)
# rot_img_mat <- array(as.numeric(unlist(rotExample$img)), dim=c(256,256,3))[,,1]
# rot_mask_mat <- array(as.numeric(unlist(rotExample$mask)), dim=c(256,256,1))[,,1]
c(img,mask) %<-%
random_rotate(
img=example[[1]][1,,,1],
mask=example[[2]][1,,,1])
# report tensor shapes
img$shape; mask$shape## TensorShape([256, 256])
## TensorShape([256, 256])
# Helper function (define once)
mat_to_df <- function(mat, name) {
expand.grid(x = 1:ncol(mat), y = 1:nrow(mat)) %>%
mutate(value = as.vector(mat),
panel = name)
}
# Assuming 'img' and 'mask' are the rotated matrices from your earlier code
bind_rows(
mat_to_df(img, "Rotated Image"),
mat_to_df(mask, "Rotated Mask")
) %>%
ggplot(aes(x = x, y = y, fill = value)) +
geom_raster() +
facet_wrap(~ panel, nrow = 1) +
scale_y_reverse() +
scale_fill_gradient(low = "black", high = "white") +
coord_fixed() +
theme_void() +
theme(legend.position = "none") +
labs(title = "Augmented / Rotated Image + Mask")The UNet DCNN model summary includes:
In this image-segmentation problem, the loss function needs
to account for the result of labeling ALL pixel intensities in the brain
images. Hence, every pixel location contributes equally to the total
loss measure. As Binary classification yields a \(1\) (tumor) or \(0\) (normal brain tissue), the
binary_crossentropy() method is one appropriate choice of a
loss function. During the iterative transfer learning process, we can
track the classification accuracy, dice coefficient or other evaluation
metrics that capture the proportion of correctly classified pixels.
Next we will define, compile and test the base (pre-training default) model, prior to model fitting. This is a naive prediction without model tuning or transfer learning.
# Model Training - Starting with a pre-trained U-net model and then expanding it with Transfer-learning
library(unet)
model <- unet::unet(input_shape = c(256, 256, 3)) # for RGB use 3-channels: input_shape = c(256, 256, 3)
summary(model)## Model: "model_1"
## ________________________________________________________________________________
## Layer (type) Output Shape Param Connected to
## #
## ================================================================================
## input_2 (InputLayer) [(None, 256, 256, 3)] 0 []
## conv2d_19 (Conv2D) (None, 256, 256, 64) 1792 ['input_2[0][0]']
## conv2d_20 (Conv2D) (None, 256, 256, 64) 36928 ['conv2d_19[0][0]']
## max_pooling2d_4 (MaxP (None, 128, 128, 64) 0 ['conv2d_20[0][0]']
## ooling2D)
## conv2d_21 (Conv2D) (None, 128, 128, 128) 73856 ['max_pooling2d_4[0][0]'
## ]
## conv2d_22 (Conv2D) (None, 128, 128, 128) 14758 ['conv2d_21[0][0]']
## 4
## max_pooling2d_5 (MaxP (None, 64, 64, 128) 0 ['conv2d_22[0][0]']
## ooling2D)
## conv2d_23 (Conv2D) (None, 64, 64, 256) 29516 ['max_pooling2d_5[0][0]'
## 8 ]
## conv2d_24 (Conv2D) (None, 64, 64, 256) 59008 ['conv2d_23[0][0]']
## 0
## max_pooling2d_6 (MaxP (None, 32, 32, 256) 0 ['conv2d_24[0][0]']
## ooling2D)
## conv2d_25 (Conv2D) (None, 32, 32, 512) 11801 ['max_pooling2d_6[0][0]'
## 60 ]
## conv2d_26 (Conv2D) (None, 32, 32, 512) 23598 ['conv2d_25[0][0]']
## 08
## max_pooling2d_7 (MaxP (None, 16, 16, 512) 0 ['conv2d_26[0][0]']
## ooling2D)
## dropout_1 (Dropout) (None, 16, 16, 512) 0 ['max_pooling2d_7[0][0]'
## ]
## conv2d_27 (Conv2D) (None, 16, 16, 1024) 47196 ['dropout_1[0][0]']
## 16
## conv2d_28 (Conv2D) (None, 16, 16, 1024) 94382 ['conv2d_27[0][0]']
## 08
## conv2d_transpose_4 (C (None, 32, 32, 512) 20976 ['conv2d_28[0][0]']
## onv2DTranspose) 64
## concatenate_4 (Concat (None, 32, 32, 1024) 0 ['conv2d_26[0][0]',
## enate) 'conv2d_transpose_4[0][
## 0]']
## conv2d_29 (Conv2D) (None, 32, 32, 512) 47191 ['concatenate_4[0][0]']
## 04
## conv2d_30 (Conv2D) (None, 32, 32, 512) 23598 ['conv2d_29[0][0]']
## 08
## conv2d_transpose_5 (C (None, 64, 64, 256) 52454 ['conv2d_30[0][0]']
## onv2DTranspose) 4
## concatenate_5 (Concat (None, 64, 64, 512) 0 ['conv2d_24[0][0]',
## enate) 'conv2d_transpose_5[0][
## 0]']
## conv2d_31 (Conv2D) (None, 64, 64, 256) 11799 ['concatenate_5[0][0]']
## 04
## conv2d_32 (Conv2D) (None, 64, 64, 256) 59008 ['conv2d_31[0][0]']
## 0
## conv2d_transpose_6 (C (None, 128, 128, 128) 13120 ['conv2d_32[0][0]']
## onv2DTranspose) 0
## concatenate_6 (Concat (None, 128, 128, 256) 0 ['conv2d_22[0][0]',
## enate) 'conv2d_transpose_6[0][
## 0]']
## conv2d_33 (Conv2D) (None, 128, 128, 128) 29504 ['concatenate_6[0][0]']
## 0
## conv2d_34 (Conv2D) (None, 128, 128, 128) 14758 ['conv2d_33[0][0]']
## 4
## conv2d_transpose_7 (C (None, 256, 256, 64) 32832 ['conv2d_34[0][0]']
## onv2DTranspose)
## concatenate_7 (Concat (None, 256, 256, 128) 0 ['conv2d_20[0][0]',
## enate) 'conv2d_transpose_7[0][
## 0]']
## conv2d_35 (Conv2D) (None, 256, 256, 64) 73792 ['concatenate_7[0][0]']
## conv2d_36 (Conv2D) (None, 256, 256, 64) 36928 ['conv2d_35[0][0]']
## conv2d_37 (Conv2D) (None, 256, 256, 1) 65 ['conv2d_36[0][0]']
## ================================================================================
## Total params: 31031745 (118.38 MB)
## Trainable params: 31031745 (118.38 MB)
## Non-trainable params: 0 (0.00 Byte)
## ________________________________________________________________________________
# define a custom loss function (dice coefficient)
dice <- custom_metric("dice", function(y_true, y_pred, smooth = 1.0) {
y_true_f <- k_flatten(y_true)
y_pred_f <- k_flatten(y_pred)
intersection <- k_sum(y_true_f * y_pred_f)
(2 * intersection + smooth) / (k_sum(y_true_f) + k_sum(y_pred_f) + smooth)
})
# Compile the DCNN model, packaging an optimizer, loss and performance metrics
model %>% compile(
optimizer = optimizer_rmsprop(learning_rate = 1e-5),
loss = "binary_crossentropy",
metrics = list(dice, metric_binary_accuracy)
)
# Naive Prediction using the default model (prior to Re-Training or Transfer Learning)
batch <- validation_dataset %>% reticulate::as_iterator() %>% reticulate::iter_next()
predictions <- predict(model, batch[[1]])## 1/1 - 19s - 19s/epoch - 19s/step
images <- tibble(
image = batch[[1]] %>% array_branch(1),
predicted_mask = predictions[,,,1] %>% array_branch(1),
mask = batch[[2]][,,,1] %>% array_branch(1)
)
# Check performance of the default base model on one case (# 22)
#### OLD PLOT_LY() VERSION
# i=22
# z1 <- ifelse(as.matrix(as.data.frame(images$predicted_mask[i])) > 0.5, 1, 0)
# pl1 <- plot_ly(z = ~ 255*as.matrix(as.data.frame(images$mask[i])[,1:256]), type="heatmap", name=paste0("Tumor Mask ", i))
# pl2 <- plot_ly(z = ~ 255*as.matrix(as.data.frame(images$image[i])[,1:256]), type="heatmap", name=paste0("Brain Image ", i))
# pl3 <- plot_ly(z = ~ z1, type="heatmap", name=paste0("Pred Mask ", i))
# subplot(pl1, pl2, pl3, nrows=1) %>% hide_colorbar()
# Assuming `batch` and `predictions` are already computed as in the original
# batch[[1]] = images, batch[[2]] = true masks, predictions = predicted masks
i <- 22 # or any index
# Extract the three matrices
img_mat <- batch[[1]][i,,,1] %>% as.array() # 1 channel for display
true_mat <- batch[[2]][i,,,1] %>% as.array()
# pred_mat <- ifelse(predictions[,,,1][,,i] > 0.5, 1, 0) # binarize
pred_mat <-
ifelse(as.matrix(as.data.frame(images$predicted_mask[i])) > 0.5, 1, 0) # binarize
# Build data frames
df_img <- expand.grid(x = 1:ncol(img_mat), y = 1:nrow(img_mat))
df_img$value <- as.vector(img_mat)
df_img$panel <- "Brain Image"
df_true <- expand.grid(x = 1:ncol(true_mat), y = 1:nrow(true_mat))
df_true$value <- as.vector(true_mat)
df_true$panel <- "True Mask"
df_pred <- expand.grid(x = 1:ncol(pred_mat), y = 1:nrow(pred_mat))
df_pred$value <- as.vector(pred_mat)
df_pred$panel <- "Predicted Mask"
bind_rows(df_img, df_true, df_pred) %>%
ggplot(aes(x = x, y = y, fill = value)) +
geom_raster() +
facet_wrap(~ panel, nrow = 1) +
scale_y_reverse() +
scale_fill_gradient(low = "black", high = "white") +
coord_fixed() +
theme_void() +
theme(legend.position = "none",
strip.text = element_text(size = 12, face = "bold")) +
labs(title = paste("Default Model Prediction – Case", i))# training_dataset <- create_dataset(training(data_train_valid), train = TRUE)
# validation_dataset <- create_dataset(testing(data_train_valid), train = FALSE)
#
# library(unet)
#
# model <- unet::unet(input_shape = c(256, 256, 3)) # for RGB use 3-channels: input_shape = c(256, 256, 3)
# # summary(model)
#
# library(keras)
# dice <- custom_metric("dice", function(y_true, y_pred, smooth = 1.0) {
# y_true_f <- k_flatten(y_true)
# y_pred_f <- k_flatten(y_pred)
# intersection <- k_sum(y_true_f * y_pred_f)
# (2 * intersection + smooth) / (k_sum(y_true_f) + k_sum(y_pred_f) + smooth)
# })
#
# model %>% compile(
# optimizer = optimizer_rmsprop(learning_rate = 1e-5),
# loss = "binary_crossentropy",
# metrics = list(dice, metric_binary_accuracy)
# )The part below shows the UNet/DCNN model fitting for a single epoch.
This block of code is not run (eval=F) as it takes hours to
complete, but below we show the results from offline DCNN trainings with
different settings.
# MODEL FITTING
epochs = 1
history <- model %>% fit(training_dataset, epochs = epochs, validation_data = validation_dataset)
# ASSESSMENT - Actual Model Evaluation (after Transfer-learning retraining)
# Naive Prediction
batch <- validation_dataset %>% reticulate::as_iterator() %>% reticulate::iter_next()
predictions <- predict(model, batch[[1]])
images <- tibble(
image = batch[[1]] %>% array_branch(1),
predicted_mask = predictions[,,,1] %>% array_branch(1),
mask = batch[[2]][,,,1] %>% array_branch(1)
)
##### OLD plot_ly() interactig display
# i=22
# pl1 <- plot_ly(z = ~ 255*as.matrix(as.data.frame(images$mask[i])[,1:256]), type="heatmap", name=paste0("Tumor Mask ", i))
# pl2 <- plot_ly(z = ~ 255*as.matrix(as.data.frame(images$image[i])[,1:256]), type="heatmap", name=paste0("Brain Image ", i))
# pl3 <- plot_ly(z = ~ 255*as.matrix(as.data.frame(images$predicted_mask[i])[,1:256]), type="heatmap", name=paste0("Pred Mask ", i))
# subplot(pl1, pl2, pl3, nrows=1)
library(ggplot2)
library(patchwork)
# After model fitting and obtaining `batch` and `predictions` as in the original
# (assuming `images` tibble is already created)
i <- 22
# Extract matrices (already scaled 0-1, we don't multiply by 255)
mask_mat <- as.matrix(as.data.frame(images$mask[i])[,1:256])
img_mat <- as.matrix(as.data.frame(images$image[i])[,1:256])
pred_mat <- as.matrix(as.data.frame(images$predicted_mask[i])[,1:256])
# Helper to convert a matrix to a ggplot heatmap
mat_to_gg <- function(mat, title) {
df <- expand.grid(x = 1:ncol(mat), y = 1:nrow(mat))
df$value <- as.vector(mat)
ggplot(df, aes(x = x, y = y, fill = value)) +
geom_raster() +
scale_fill_gradient(low = "black", high = "white") +
coord_fixed() +
theme_void() +
theme(legend.position = "none",
plot.title = element_text(hjust = 0.5, size = 12)) +
labs(title = title)
}
# Create three plots
p_mask <- mat_to_gg(mask_mat, paste0("Tumor Mask ", i))
p_img <- mat_to_gg(img_mat, paste0("Brain Image ", i))
p_pred <- mat_to_gg(pred_mat, paste0("Pred Mask ", i))
# Combine side by side
(p_mask | p_img | p_pred) +
plot_annotation(title = paste("Model Evaluation after Transfer Learning – Case", i))
library(keras)
dice <- custom_metric("dice", function(y_true, y_pred, smooth = 1.0) {
y_true_f <- k_flatten(y_true)
y_pred_f <- k_flatten(y_pred)
intersection <- k_sum(y_true_f * y_pred_f)
(2 * intersection + smooth) / (k_sum(y_true_f) + k_sum(y_pred_f) + smooth)
})
model %>% compile(
optimizer = optimizer_rmsprop(learning_rate = 1e-5),
loss = "binary_crossentropy",
metrics = list(dice, metric_binary_accuracy)
)When training the DCNN model, keep in mind that this step is very computationally intensive (each epoch can take hours to complete). Running a small number of epochs may be feasible in an interactive RStudio session, but dozens of epochs are necessary to train the network to perform well (e.g., yield high Dice coefficient values).
# MODEL FITTING
history <- model %>% fit(training_dataset, epochs = 1, validation_data = validation_dataset, verbose = 0)
# 81/81 [==============================] - 3313s 41s/step -
# loss: 0.3121 - dice: 0.0106 - binary_accuracy: 0.9604 - val_loss: 0.1762 - val_dice: 1.7162e-04 - val_binary_accuracy: 0.9900
# EVALUATION
pl_loss <- plot_ly(x = ~c(1:history$params$epochs), y = ~history$metrics$loss,
type = "scatter", mode="markers+lines", name="Loss") %>%
add_trace(x = ~c(1:history$params$epochs), y = ~history$metrics$val_loss,
type = "scatter", mode="markers+lines", name="Validation Loss") %>%
layout(title="DNN Training/Validation Performance", xaxis=list(title="epoch"),
yaxis=list(title="Metric Value"), legend = list(orientation='h'))
pl_acc <- plot_ly(x = ~c(1:history$params$epochs), y = ~history$metrics$binary_accuracy,
type = "scatter", mode="markers+lines", name="Accuracy") %>%
add_trace(x = ~c(1:history$params$epochs), y = ~history$metrics$binary_accuracy,
type = "scatter", mode="markers+lines", name="Validation Binary Accuracy") %>%
layout(title="DNN Training/Validation Performance", xaxis=list(title="epoch"),
yaxis=list(title="Metric Value"), legend = list(orientation='h'))
subplot(pl_loss, pl_acc, nrows=2, shareX = TRUE, titleX = TRUE)
# model %>% evaluate(training_dataset %>% dataset_batch(25), verbose = 0)
model %>% evaluate(validation_dataset, verbose = 0)
# loss dice binary_accuracy
# 0.1762175262 0.0001716204 0.9899647236
# Save/Load the pretrained model in HDF5 format
save_model_hdf5(model, "C:/Users/IvoD/Desktop/model_TF_Brain_epoch_1.h5",
overwrite = TRUE, include_optimizer = TRUE)
# Mind that loading a model with custom layers/functions, e.g., dice() method, requires special import using a list
# https://github.com/rstudio/keras/issues/1240Once we have pre-trained
(history <- model %>% fit()) and saved
(save_model_hdf5()) the model (model), we can
load it back as mod1 in the interactive session and use it
for prediction. Several pre-trained
models (e.g., 50 and 100 epochs) are available on the Canvas site.
Below, we are using one of the pre-trained Unet models
(model_TF_Brain_epoch_100.h5, 250MB) to illustrate the
prediction performance, i.e., automated brain-tumor segmentation using
2D neuroimaging data.
Notice the steady model improvement (accuracy, binary tumor-labeling, and dice-coefficient) with respect to the epoch index.
# # This HDF5 model is available on Canvas:
# # https://umich.instructure.com/courses/38100/files/folder/Case_Studies/36_TCGA_MRI_Segmentation_Data_Phenotypes
# # https://umich.instructure.com/files/22559456/download?download_frd=1
# mod1 <-
# load_model_hdf5("E:/Ivo.dir/Research/UMichigan/Proposals/2014/UMich_MINDS_DataSciInitiative_2014/2016/MIDAS_HSXXX_DataScienceAnalyticsCourse_Porposal/MOOC_Implementation/appendix/model_TF_Brain_epoch_50.h5",
# custom_objects = list("dice" = dice), compile = TRUE)
#
# # Update the model's last layer: https://tensorflow.rstudio.com/guide/keras/faq/
# # mod2 <- mod1 %>%
# # # get_layer(index=33) %>%
# # pop_layer() %>% # remove the output layer first, then plug in a new output layer
# # # layer_conv_2d(filters = 64, kernel_size = c(3, 3), padding = "same")
# # layer_conv_2d(filters = 64, kernel_size = c(3,3), activation = "relu")
# input <- mod1$input # UNet input layer
# base <- (mod1 %>% get_layer(index=33))$output # Get the last output layer of "mod1"
# target <- base %>% # Replace the output layer by a new conv2D layer outputting a 3-channel 2D brain image
# layer_conv_2d(filters = 3, kernel_size = c(1,1), activation = "relu")
# transfer_model <- keras_model(input, target) # Update mod1 for transfer learning
# summary(transfer_model)
# # Mind the final layer change:
# # Layer (type) Output Shape Param # Connected to
# # conv2d_23 (Conv2D) (None, 256, 256, 3) 6 conv2d_18[0][0]
# # Define the Dice coefficient
# dice <- custom_metric("dice", function(y_true, y_pred, smooth = 1.0) {
# y_true_f <- k_flatten(y_true)
# y_pred_f <- k_flatten(y_pred)
# intersection <- k_sum(y_true_f * y_pred_f)
# (2 * intersection + smooth) / (k_sum(y_true_f) + k_sum(y_pred_f) + smooth)
# })
#
# # Recompile the model with our custom dice() loss function
# mod1 %>% compile(
# optimizer = optimizer_rmsprop(learning_rate = 1e-5),
# loss = "binary_crossentropy",
# metrics = list(dice, metric_binary_accuracy)
# )
# # Check re-loaded mod1 model and evaluate performance metrics on validation dataset
# # summary(mod1)
# mod1 %>% evaluate(validation_dataset)
#
# # Finally display some of the results -- ASSESSMENT -- Actual Model Evaluation (after Transfer-learning retraining)
# batch <- validation_dataset %>% reticulate::as_iterator() %>% reticulate::iter_next()
# predictions <- predict(mod1, batch[[1]])
# images <- tibble(
# image = batch[[1]] %>% array_branch(1),
# predicted_mask = predictions[,,,1] %>% array_branch(1),
# mask = batch[[2]][,,,1] %>% array_branch(1)
# )
# i=16
# pl1 <- plot_ly(z = ~ 255*as.matrix(as.data.frame(images$mask[i])[,1:256]), type="heatmap", name=paste0("Tumor Mask ", i)) %>%
# layout(hovermode = "y unified", xaxis = list(hoverformat = '.1f'), yaxis = list(hoverformat = '.1f'))
# pl2 <- plot_ly(z = ~ 255*as.matrix(as.data.frame(images$image[i])[,1:256]), type="heatmap", name=paste0("Brain Image ", i)) %>%
# layout(hovermode = "y unified", xaxis = list(hoverformat = '.1f'), yaxis = list(hoverformat = '.1f'))
# pl3 <- plot_ly(z = ~ 255*as.matrix(as.data.frame(images$predicted_mask[i])[,1:256]), type="heatmap", name=paste0("Pred Mask ", i)) %>%
# layout(hovermode = "y unified", xaxis = list(hoverformat = '.1f'), yaxis = list(hoverformat = '.1f'))
# subplot(pl1, pl2, pl3, nrows=1, shareY = TRUE) %>%
# layout(title=paste0("Case ", i))
# batch <- validation_dataset %>% reticulate::as_iterator() %>% reticulate::iter_next()
# predictions <- predict(mod1, batch)
# str(predictions) # num [1:32, 1:256, 1:256, 1] 0.466 0.463 0.48 0.449 0.494 ...
# hist(predictions)
#
# images <- tibble(
# image = batch[[1]] %>% array_branch(1),
# predicted_mask = predictions[,,,1] %>% array_branch(1),
# mask = batch[[2]][,,,1] %>% array_branch(1)
# )
# Display the Transfer-Learning Model Performance across epochs
# These metrics are precomputed on SOCR-Lighthouse server, as the transfer-learning fine-tuning training
# takes days to go through 50 epochs and dozens of iterations per epoch
## https://umich.instructure.com/courses/38100/files/folder/Case_Studies/36_TCGA_MRI_Segmentation_Data_Phenotypes
##
# epochs <- 48
# # Import the metrics: Loss, Dice, Binary_Accuracy, Validation_Loss, Validation_Dice, Validation_Binary_Accuracy
# historyMetrics_epochs48 <- read.csv("https://umich.instructure.com/files/22559501/download?download_frd=1", stringsAsFactors = F)
# hist_df <- data.frame(epoch=historyMetrics_epochs48$Epoch, loss=historyMetrics_epochs48$Loss,
# dice=historyMetrics_epochs48$Dice, binAccuracy=historyMetrics_epochs48$Binary_Accuracy,
# valid_loss=historyMetrics_epochs48$Validation_Loss, valid_dice=historyMetrics_epochs48$Validation_Dice,
# valid_binAccuracy=historyMetrics_epochs48$Validation_Binary_Accuracy)
#
# p_loss <- plot_ly(hist_df, x = ~epoch) %>%
# add_trace(y = ~loss, name = 'Training Loss', type="scatter", mode = 'lines+markers') %>%
# add_trace(y = ~valid_loss, name = 'Validation Loss', type="scatter", mode = 'lines+markers')
# p_acc <- plot_ly(hist_df, x = ~epoch) %>%
# add_trace(y = ~dice, name = 'Training Dice', type="scatter", mode = 'lines+markers') %>% # DICE
# add_trace(y = ~valid_dice, name = 'Validation Accuracy', type="scatter", mode = 'lines+markers') %>%
# add_trace(y = ~binAccuracy, name = 'Training Binary-Accuracy', type="scatter", mode = 'lines+markers') %>% # ACCURACY
# add_trace(y = ~valid_binAccuracy, name = 'Validation Binary-Accuracy', type="scatter", mode = 'lines+markers')
# subplot(p_loss, p_acc, nrows=2) %>%
# layout(legend = list(orientation = 'h'), title="Tensorflow Transfer-Learning Performance (Brain Imaging), Epochs=48")
# # MODEL FITTING on Lighthouse SOCR Compute Server
# epochs = 100
# history <- model %>% fit(training_dataset, epochs = epochs, validation_data = validation_dataset)
#
# # ASSESSMENT - Actual Model Evaluation (after Transfer-learning retraining)
# # Naive Prediction
# batch <- validation_dataset %>% reticulate::as_iterator() %>% reticulate::iter_next()
# predictions <- predict(model, batch[[1]])
# images <- tibble(
# image = batch[[1]] %>% array_branch(1),
# predicted_mask = predictions[,,,1] %>% array_branch(1),
# mask = batch[[2]][,,,1] %>% array_branch(1)
# )
# # i=22
# # pl1 <- plot_ly(z = ~ 255*as.matrix(as.data.frame(images$mask[i])[,1:256]), type="heatmap", name=paste0("Tumor Mask ", i))
# # pl2 <- plot_ly(z = ~ 255*as.matrix(as.data.frame(images$image[i])[,1:256]), type="heatmap", name=paste0("Brain Image ", i))
# # pl3 <- plot_ly(z = ~ 255*as.matrix(as.data.frame(images$predicted_mask[i])[,1:256]), type="heatmap", name=paste0("Pred Mask ", i))
# # subplot(pl1, pl2, pl3, nrows=1)
#
#
# # MODEL FITTING
# # history <- model %>% fit(training_dataset, epochs = 1, validation_data = validation_dataset, verbose = 2)
#
# # 81/81 [==============================] - 3313s 41s/step -
# # loss: 0.3121 - dice: 0.0106 - binary_accuracy: 0.9604 - val_loss: 0.1762 - val_dice: 1.7162e-04 - val_binary_accuracy: 0.9900
#
# # EVALUATION
# # pl_loss <- plot_ly(x = ~c(1:history$params$epochs), y = ~history$metrics$loss,
# # type = "scatter", mode="markers+lines", name="Loss") %>%
# # add_trace(x = ~c(1:history$params$epochs), y = ~history$metrics$val_loss,
# # type = "scatter", mode="markers+lines", name="Validation Loss") %>%
# # layout(title="DNN Training/Validation Performance", xaxis=list(title="epoch"),
# # yaxis=list(title="Metric Value"), legend = list(orientation='h'))
# #
# # pl_acc <- plot_ly(x = ~c(1:history$params$epochs), y = ~history$metrics$binary_accuracy,
# # type = "scatter", mode="markers+lines", name="Accuracy") %>%
# # add_trace(x = ~c(1:history$params$epochs), y = ~history$metrics$binary_accuracy,
# # type = "scatter", mode="markers+lines", name="Validation Binary Accuracy") %>%
# # layout(title="DNN Training/Validation Performance", xaxis=list(title="epoch"),
# # yaxis=list(title="Metric Value"), legend = list(orientation='h'))
# #
# # subplot(pl_loss, pl_acc, nrows=2, shareX = TRUE, titleX = TRUE)
#
# # model %>% evaluate(training_dataset %>% dataset_batch(25), verbose = 0)
# model %>% evaluate(validation_dataset, verbose = 0)
# # loss dice binary_accuracy
# # 0.1762175262 0.0001716204 0.9899647236
#
# # Save/Load the pretrained model in HDF5 format
# save_model_hdf5(model, "/home/dinov/DSPA/test_code/Chap22_DCNN_UNet_modeling/model_TF_Brain_epoch_100.h5",
# overwrite = TRUE, include_optimizer = TRUE)
# # Save History object tracking model performance across epochs,
# # see https://cran.r-project.org/web/packages/keras/vignettes/training_visualization.html
# history_df <- as.data.frame(history)
# # str(history_df)
# # 'data.frame': 120 obs. of 4 variables:
# # $ epoch : int 1 2 3 4 5 6 7 8 9 10 ...
# # $ value : num 0.87 0.941 0.954 0.962 0.965 ...
# # $ metric: Factor w/ 2 levels "acc","loss": 1 1 1 1 1 1 1 1 1 1 ...
# # $ data : Factor w/ 2 levels "training","validation": 1 1 1 1 1 1 1 1 1 1 ...
# save(history_df, file="/home/dinov/DSPA/test_code/Chap22_DCNN_UNet_modeling/historyModel_TF_Brain_Epochs50.Rda")
# # Then load it with:
# #
# REPORT Training and Validation Performance: load the performance metrics (history_df) over 100 epochs
# https://umich.instructure.com/courses/38100/files/folder/Case_Studies/36_TCGA_MRI_Segmentation_Data_Phenotypes | historyModel_TF_Brain_Epochs100.Rda
# load(url("https://umich.instructure.com/files/22647027/download?download_frd=1"))
# 1. Define the URL and the temporary destination name
# file_url <- "https://umich.instructure.com/files/22647027/download?download_frd=1"
dest_file <- "D:/IvoD/Desktop/Ivo.dir/Research/UMichigan/Publications_Books/2023/DSPA_Springer_2nd_Edition_2023/Rmd_HTML/historyModel_TF_Brain_Epochs50.Rda"
# # 2. Download the file securely to your machine
# download.file(file_url, destfile = dest_file, mode = "wb")
# 3. Load the local file
load(dest_file)
lineWidth <- ifelse(history_df$data=='training', 1, 4)
lineNames <- paste0(history_df$data, " ", history_df$metric)
plot_ly(history_df, x = ~epoch, y=~value, color=~metric, type="scatter", mode="lines+markers", name=~lineNames,
line = list(color = ~metric, width = lineWidth)) %>%
layout(legend = list(orientation = 'h'), title="Tensorflow Transfer-Learning Performance (Brain Imaging), Epochs=100")# PREDICTION
# Mind that loading a model with custom layers/functions, e.g., dice() method, requires special import using a list
# https://github.com/rstudio/keras/issues/1240
# Once we have trained (`history <- model %>% fit(()`) and saved (`save_model_hdf5()`) the model (`model`), we can load it back as `mod1` the interactive session and use it for prediction.
# Define the Dice coefficient
dice <- custom_metric("dice", function(y_true, y_pred, smooth = 1.0) {
y_true_f <- k_flatten(y_true)
y_pred_f <- k_flatten(y_pred)
intersection <- k_sum(y_true_f * y_pred_f)
(2 * intersection + smooth) / (k_sum(y_true_f) + k_sum(y_pred_f) + smooth)
})
# The pre-computed model_TF_Brain_epoch_100.h5 model file is available in Canvas:
# https://umich.instructure.com/courses/38100/files/folder/Case_Studies/36_TCGA_MRI_Segmentation_Data_Phenotypes
# localModelsFolder <- "C:/Users/IvoD/Desktop/Ivo.dir/Research/UMichigan/Proposals/2014/UMich_MINDS_DataSciInitiative_2014/2016/MIDAS_HSXXX_DataScienceAnalyticsCourse_Porposal/MOOC_Implementation/appendix/"
localModelsFolder <- getwd()
mod1 <- load_model_hdf5(paste0(localModelsFolder, "/appendix/model_TF_Brain_epoch_100.h5"), compile = FALSE)
# custom_objects = list("dice" = dice), compile = TRUE)
# # Recompile the model with our custom dice() loss function
mod1 %>% compile(
optimizer = optimizer_rmsprop(learning_rate = 1e-5),
loss = "binary_crossentropy",
metrics = list(dice, metric_binary_accuracy)
)
# Check re-loaded mod1 model and evaluate performance metrics on validation dataset
# summary(mod1)
mod1 %>% evaluate(validation_dataset)## 21/21 - 368s - loss: 0.0185 - dice: 0.6665 - binary_accuracy: 0.9938 - 368s/epoch - 18s/step
## loss dice binary_accuracy
## 0.01854581 0.66647464 0.99375749
# 16/16 [==============================] - 180s 11s/step - loss: 0.0213 - dice: 0.6842 - binary_accuracy: 0.9932
# loss dice binary_accuracy
# 0.02131428 0.68422282 0.99317247
# Finally display some of the results -- ASSESSMENT -- Actual Model Evaluation (after Transfer-learning retraining)
batch <- validation_dataset %>% reticulate::as_iterator() %>% reticulate::iter_next()
predictions <- predict(mod1, batch[[1]])## 1/1 - 18s - 18s/epoch - 18s/step
images <- tibble(
image = batch[[1]] %>% array_branch(1),
predicted_mask = predictions[,,,1] %>% array_branch(1),
mask = batch[[2]][,,,1] %>% array_branch(1)
)
# i=16
# pl1 <- plot_ly(z = ~ 255*as.matrix(as.data.frame(images$mask[i])[,1:256]), type="heatmap", name=paste0("Tumor Mask ", i)) %>%
# layout(hovermode = "y unified", xaxis = list(hoverformat = '.1f'), yaxis = list(hoverformat = '.1f'))
# pl2 <- plot_ly(z = ~ 255*as.matrix(as.data.frame(images$image[i])[,1:256]), type="heatmap", name=paste0("Brain Image ", i)) %>%
# layout(hovermode = "y unified", xaxis = list(hoverformat = '.1f'), yaxis = list(hoverformat = '.1f'))
# pl3 <- plot_ly(z = ~ 255*as.matrix(as.data.frame(images$predicted_mask[i])[,1:256]), type="heatmap", name=paste0("Pred Mask ", i)) %>%
# layout(hovermode = "y unified", xaxis = list(hoverformat = '.1f'), yaxis = list(hoverformat = '.1f'))
# subplot(pl1, pl2, pl3, nrows=1, shareY = TRUE) %>%
# layout(title=paste0("DCNN HDF5 Model (epochs=100) Validation on Case ", i))
#### Old plot_ly() version - only test in interactive mode!
# pl_list <- list()
# n = 10
# for (i in 1:n) { # to limit to only 1-channel, restrict the column range to 1:256: images$mask[i])[,1:256]
# pl1 <- plot_ly(z = ~ 255*as.matrix(as.data.frame(images$mask[i])), type="heatmap", name=paste0("Tumor Mask ", i)) %>%
# layout(hovermode = "y unified", xaxis = list(hoverformat = '.1f'), yaxis = list(hoverformat = '.1f'))
# pl2 <- plot_ly(z = ~ 255*as.matrix(as.data.frame(images$image[i])), type="heatmap", name=paste0("Brain Image ", i)) %>%
# layout(hovermode = "y unified", xaxis = list(hoverformat = '.1f'), yaxis = list(hoverformat = '.1f'))
# pl3 <- plot_ly(z = ~ 255*as.matrix(as.data.frame(images$predicted_mask[i])), type="heatmap", name=paste0("Pred Mask ", i)) %>%
# layout(hovermode = "y unified", xaxis = list(hoverformat = '.1f'), yaxis = list(hoverformat = '.1f'))
# pl4 <- plot_ly(z = ~ 255*as.matrix(as.data.frame(images$mask[i+n])), type="heatmap", name=paste0("Tumor Mask ", i+n)) %>%
# layout(hovermode = "y unified", xaxis = list(hoverformat = '.1f'), yaxis = list(hoverformat = '.1f'))
# pl5 <- plot_ly(z = ~ 255*as.matrix(as.data.frame(images$image[i+n])), type="heatmap", name=paste0("Brain Image ", i+n)) %>%
# layout(hovermode = "y unified", xaxis = list(hoverformat = '.1f'), yaxis = list(hoverformat = '.1f'))
# pl6 <- plot_ly(z = ~ 255*as.matrix(as.data.frame(images$predicted_mask[i+n])), type="heatmap", name=paste0("Pred Mask ", i+n)) %>%
# layout(hovermode = "y unified", xaxis = list(hoverformat = '.1f'), yaxis = list(hoverformat = '.1f'))
# pl_list[[i]] <- subplot(pl1, pl2, pl3, pl4, pl5, pl6, nrows=1, shareY = TRUE) %>%
# layout(title=paste0("DCNN HDF5 Model (epochs=100) Validation on Case ", i)) %>% hide_colorbar()
# }
# pl_list %>%
# subplot(nrows = length(pl_list)) %>%
# layout(title=paste0("DCNN HDF5 Model (epochs=100) Predictions (N=", 2*n, " Cases)"))
# Assuming `images` tibble (with columns image, predicted_mask, mask) exists
# and `n = 10` is set
n <- 10 # or however many cases you want
all_cases <- map_dfr(1:n, function(i) {
# For each case, create a data frame for the three columns
img_mat <- as.matrix(as.data.frame(images$image[i]))[,1:256]
true_mat <- as.matrix(as.data.frame(images$mask[i]))[,1:256]
pred_mat <- as.matrix(as.data.frame(images$predicted_mask[i]))[,1:256]
bind_rows(
expand.grid(x = 1:ncol(img_mat), y = 1:nrow(img_mat)) %>%
mutate(value = as.vector(img_mat),
case = paste0("Case ", i),
type = "Brain Image"),
expand.grid(x = 1:ncol(true_mat), y = 1:nrow(true_mat)) %>%
mutate(value = as.vector(true_mat),
case = paste0("Case ", i),
type = "True Mask"),
expand.grid(x = 1:ncol(pred_mat), y = 1:nrow(pred_mat)) %>%
mutate(value = as.vector(pred_mat),
case = paste0("Case ", i),
type = "Predicted Mask")
)
})
ggplot(all_cases, aes(x = x, y = y, fill = value)) +
geom_raster() +
facet_grid(case ~ type) + # rows = cases, columns = image types
scale_y_reverse() +
scale_fill_gradient(low = "black", high = "white") +
coord_fixed() +
theme_void() +
theme(legend.position = "none",
strip.text = element_text(size = 10, face = "bold"),
strip.background = element_blank()) +
labs(title = paste0("DCNN HDF5 Model (epochs=100) Predictions – ", n, " Cases"))# batch <- validation_dataset %>% reticulate::as_iterator() %>% reticulate::iter_next()
# predictions <- predict(mod1, batch)
# str(predictions) # num [1:32, 1:256, 1:256, 1] 0.466 0.463 0.48 0.449 0.494 ...
# hist(predictions)
#
# images <- tibble(
# image = batch[[1]] %>% array_branch(1),
# predicted_mask = predictions[,,,1] %>% array_branch(1),
# mask = batch[[2]][,,,1] %>% array_branch(1)
# )
# Save Predictions on validation data
# batch <- validation_dataset %>% reticulate::as_iterator() %>% reticulate::iter_next()
# predictions <- predict(mod1, batch[[1]])
PredictedMasks <- list(predictions=predictions, images=images)
save(PredictedMasks, file="C:/Users/IvoD/Desktop/predictionsModel_TF_Brain_Epochs100.Rda")
# Then load it with:
# load("C:/Users/Dinov/Desktop/predictionsModel_TF_Brain_Epochs100.Rda")
# https://umich.instructure.com/courses/38100/files/folder/Case_Studies/36_TCGA_MRI_Segmentation_Data_Phenotypes
# load(url("https://umich.instructure.com/files/22648924/download?download_frd=1"))Finally, we can explicate some of the Transfer Learning
process by modifying the pretrained model mod2 and
obtaining a new model, modTransferLearning, which
demonstrates synthetic image generation. This new modified
modTransferLearning DCNN will output 3-channel
brain-images, not tumor masks (predicted by mod2). Note
that 31M parameters part of the original pretrained mod2
are fixed and we are only estimating the remaining \(16K\) parameters in this transfer-learning
tuning process.
# This HDF5 model (model_TF_Brain_epoch_100.h5) is available on Canvas:
# https://umich.instructure.com/courses/38100/files/folder/Case_Studies/36_TCGA_MRI_Segmentation_Data_Phenotypes
# https://umich.instructure.com/files/22647026/download?download_frd=1
mod2 <- load_model_hdf5(paste0(localModelsFolder, "/appendix/model_TF_Brain_epoch_100.h5"), compile=FALSE)
# custom_objects = list("dice" = dice), compile = TRUE)
# mod2 <- load_model_hdf5("E:/Ivo.dir/Research/UMichigan/Proposals/2014/UMich_MINDS_DataSciInitiative_2014/2016/MIDAS_HSXXX_DataScienceAnalyticsCourse_Porposal/MOOC_Implementation/appendix/model_TF_Brain_epoch_100.h5", custom_objects = list("dice" = dice), compile = TRUE)
# Freeze the first 32 layers and inspect the number of trainable and non-trainable (frozen DCNN parameters)
for (layer in mod2$layers) layer$trainable <- FALSE
# summary(mod2); length(mod2$layers)
# Update the model's last layer: https://tensorflow.rstudio.com/guide/keras/faq/
# More about Keras layers: https://keras.rstudio.com/reference/index.html#section-core-layers
# mod2 <- mod1 %>%
# # get_layer(index=33) %>%
# pop_layer() %>% # remove the output layer first, then plug in a new output layer
# # layer_conv_2d(filters = 64, kernel_size = c(3, 3), padding = "same")
# layer_conv_2d(filters = 64, kernel_size = c(3,3), activation = "relu")
input <- mod2$input # Pretrained UNet input layer
base <- (mod2 %>% get_layer(index=32))$output # Get the prior-to-last output layer of "mod2", total #layers =33
target <- base %>% # Replace the output layer by a new conv2D layer outputting a 3-channel 2D brain image, not a mask
layer_conv_2d(filters = 64, kernel_size = c(1,1), activation = "relu") %>%
layer_max_pooling_2d(pool_size = c(2,2)) %>%
layer_conv_2d(filters = 64, kernel_size = c(1,1), activation = "relu") %>%
layer_max_pooling_2d(pool_size = c(2,2)) %>%
layer_conv_2d_transpose(filters = 32, kernel_size = c(2,2), strides = 2, padding = "same", activation = "relu") %>% # deconvolution layers
layer_conv_2d_transpose(filters = 3, kernel_size = c(2,2), strides = 2, padding = "same", activation = "relu")
modTransferLearning <- keras_model(input, target) # Update mod2 for transfer learning
summary(modTransferLearning); length(modTransferLearning$layers)## Model: "model_2"
## ________________________________________________________________________________
## Layer (type) Output Shape Para Connected to Trainable
## m #
## ================================================================================
## input_1 (InputLay [(None, 256, 256, 0 [] N
## er) 3)]
## conv2d (Conv2D) (None, 256, 256, 6 1792 ['input_1[0][0]'] N
## 4)
## conv2d_1 (Conv2D) (None, 256, 256, 6 3692 ['conv2d[0][0]'] N
## 4) 8
## max_pooling2d (Ma (None, 128, 128, 6 0 ['conv2d_1[0][0]'] N
## xPooling2D) 4)
## conv2d_2 (Conv2D) (None, 128, 128, 1 7385 ['max_pooling2d[0] N
## 28) 6 [0]']
## conv2d_3 (Conv2D) (None, 128, 128, 1 1475 ['conv2d_2[0][0]'] N
## 28) 84
## max_pooling2d_1 ( (None, 64, 64, 128 0 ['conv2d_3[0][0]'] N
## MaxPooling2D) )
## conv2d_4 (Conv2D) (None, 64, 64, 256 2951 ['max_pooling2d_1[ N
## ) 68 0][0]']
## conv2d_5 (Conv2D) (None, 64, 64, 256 5900 ['conv2d_4[0][0]'] N
## ) 80
## max_pooling2d_2 ( (None, 32, 32, 256 0 ['conv2d_5[0][0]'] N
## MaxPooling2D) )
## conv2d_6 (Conv2D) (None, 32, 32, 512 1180 ['max_pooling2d_2[ N
## ) 160 0][0]']
## conv2d_7 (Conv2D) (None, 32, 32, 512 2359 ['conv2d_6[0][0]'] N
## ) 808
## max_pooling2d_3 ( (None, 16, 16, 512 0 ['conv2d_7[0][0]'] N
## MaxPooling2D) )
## dropout (Dropout) (None, 16, 16, 512 0 ['max_pooling2d_3[ N
## ) 0][0]']
## conv2d_8 (Conv2D) (None, 16, 16, 102 4719 ['dropout[0][0]'] N
## 4) 616
## conv2d_9 (Conv2D) (None, 16, 16, 102 9438 ['conv2d_8[0][0]'] N
## 4) 208
## conv2d_transpose (None, 32, 32, 512 2097 ['conv2d_9[0][0]'] N
## (Conv2DTranspose) ) 664
## concatenate (Conc (None, 32, 32, 102 0 ['conv2d_7[0][0]', N
## atenate) 4) 'conv2d_transpose
## [0][0]']
## conv2d_10 (Conv2D (None, 32, 32, 512 4719 ['concatenate[0][0 N
## ) ) 104 ]']
## conv2d_11 (Conv2D (None, 32, 32, 512 2359 ['conv2d_10[0][0]' N
## ) ) 808 ]
## conv2d_transpose_ (None, 64, 64, 256 5245 ['conv2d_11[0][0]' N
## 1 (Conv2DTranspos ) 44 ]
## e)
## concatenate_1 (Co (None, 64, 64, 512 0 ['conv2d_5[0][0]', N
## ncatenate) ) 'conv2d_transpose
## _1[0][0]']
## conv2d_12 (Conv2D (None, 64, 64, 256 1179 ['concatenate_1[0] N
## ) ) 904 [0]']
## conv2d_13 (Conv2D (None, 64, 64, 256 5900 ['conv2d_12[0][0]' N
## ) ) 80 ]
## conv2d_transpose_ (None, 128, 128, 1 1312 ['conv2d_13[0][0]' N
## 2 (Conv2DTranspos 28) 00 ]
## e)
## concatenate_2 (Co (None, 128, 128, 2 0 ['conv2d_3[0][0]', N
## ncatenate) 56) 'conv2d_transpose
## _2[0][0]']
## conv2d_14 (Conv2D (None, 128, 128, 1 2950 ['concatenate_2[0] N
## ) 28) 40 [0]']
## conv2d_15 (Conv2D (None, 128, 128, 1 1475 ['conv2d_14[0][0]' N
## ) 28) 84 ]
## conv2d_transpose_ (None, 256, 256, 6 3283 ['conv2d_15[0][0]' N
## 3 (Conv2DTranspos 4) 2 ]
## e)
## concatenate_3 (Co (None, 256, 256, 1 0 ['conv2d_1[0][0]', N
## ncatenate) 28) 'conv2d_transpose
## _3[0][0]']
## conv2d_16 (Conv2D (None, 256, 256, 6 7379 ['concatenate_3[0] N
## ) 4) 2 [0]']
## conv2d_17 (Conv2D (None, 256, 256, 6 3692 ['conv2d_16[0][0]' N
## ) 4) 8 ]
## conv2d_39 (Conv2D (None, 256, 256, 6 4160 ['conv2d_17[0][0]' Y
## ) 4) ]
## max_pooling2d_9 ( (None, 128, 128, 6 0 ['conv2d_39[0][0]' Y
## MaxPooling2D) 4) ]
## conv2d_38 (Conv2D (None, 128, 128, 6 4160 ['max_pooling2d_9[ Y
## ) 4) 0][0]']
## max_pooling2d_8 ( (None, 64, 64, 64) 0 ['conv2d_38[0][0]' Y
## MaxPooling2D) ]
## conv2d_transpose_ (None, 128, 128, 3 8224 ['max_pooling2d_8[ Y
## 9 (Conv2DTranspos 2) 0][0]']
## e)
## conv2d_transpose_ (None, 256, 256, 3 387 ['conv2d_transpose Y
## 8 (Conv2DTranspos ) _9[0][0]']
## e)
## ================================================================================
## Total params: 31048611 (118.44 MB)
## Trainable params: 16931 (66.14 KB)
## Non-trainable params: 31031680 (118.38 MB)
## ________________________________________________________________________________
## [1] 38
# 31M parameters are fixed and we are only estimating 16K parameters in the Transfer-Learning tuning.
# Inspect the number of trainable and non-trainable (frozen DCNN parameters)
# ....
# ...
# conv2d_17 (Conv2D) (None, 256, 256, 64) 36928 conv2d_16[0][0] # Last Layers of base model mod2
# ___________________________________________________________________________________________
# conv2d_44 (Conv2D) (None, 256, 256, 64) 4160 conv2d_17[0][0] # First new TF layer
# ___________________________________________________________________________________________
# max_pooling2d_25 (MaxPooling2D) (None, 128, 128, 64) 0 conv2d_44[0][0]
# ___________________________________________________________________________________________
# conv2d_43 (Conv2D) (None, 128, 128, 64) 4160 max_pooling2d_25[0][0]
# ___________________________________________________________________________________________
# max_pooling2d_24 (MaxPooling2D) (None, 64, 64, 64) 0 conv2d_43[0][0]
# ___________________________________________________________________________________________
# conv2d_transpose_17 (Conv2DTranspose) (None, 128, 128, 32) 8224 max_pooling2d_24[0][0]
# ____________________________________________________________________________________________
# conv2d_transpose_16 (Conv2DTranspose) (None, 256, 256, 3) 387 v2d_transpose_17[0][0] # New Last Output Layer of TL model modTransferLearning
# ============================================================================================
# Total params: 31,048,611
# Trainable params: 16,931
# Non-trainable params: 31,031,680
# ________________________________
# [1] 38 # Layers of new modTransferLearning model
## MODEL RE-TRAINING (for Transfer Learning)
# Compile the DCNN model, packaging an optimizer, loss and performance metrics
modTransferLearning %>% compile(
optimizer = optimizer_rmsprop(learning_rate = 1e-5),
# loss = 'loss_kullback_leibler_divergence',
loss = 'mse', # as we are looking at minimizing ||OrigImage - SynthImage||, i.e., the RMSE
metrics = list(metric_mean_absolute_error, metric_poisson)
)
currentRotationIndex <- 1 # reset
### PREP New Data (replace masks by the native brain images)
create_TL_dataset <- function(data, train, batch_size = 32L) {
dataset <- data %>% # load all PNG files as images
tensor_slices_dataset() %>%
dataset_map(~.x %>% list_modify(
img = tf$image$decode_png(tf$io$read_file(.x$img), channels = 3), # <------ IMG
mask= tf$image$decode_png(tf$io$read_file(.x$img), channels = 3) # <------ MASK == IMG
)) %>% # convert image intensities from int8 to 32-bit float
dataset_map(~.x %>% list_modify(
img = tf$image$convert_image_dtype(.x$img, dtype = tf$float32),
mask= tf$image$convert_image_dtype(.x$mask, dtype = tf$float32)
)) %>% # reshape all tensor-shape to (256 * 256) to ensure spatial-index homologies
dataset_map(~.x %>% list_modify(
img = tf$image$resize(.x$img, size = shape(256, 256)),
mask= tf$image$resize(.x$mask, size = shape(256, 256))
))
# data augmentation performed on training set only
if (train) {
dataset <- dataset %>%
dataset_map(~.x %>% list_modify(
img = random_bsh(.x$img) # ,
# mask = img
)) %>%
dataset_map(~.x %>% list_modify( # see discussion in create_dataset()
img = random_rotate(img=.x$img, mask=.x$mask)$img,
mask= random_rotate(img=.x$img, mask=.x$mask)$mask
))
}
# shuffling on training set only
if (train) {
dataset <- dataset %>%
dataset_shuffle(buffer_size = batch_size*4)
}
# train in batches; batch size might need to be adapted depending on
# available memory
dataset <- dataset %>%
dataset_batch(batch_size)
dataset %>%
# output needs to be unnamed
dataset_map(unname)
}
# Generate the Transfer Learning (TL) Training and Testing data at iterated TF-Data objects
training_TL_dataset <- create_TL_dataset(training(data_train_valid), train = TRUE)
validation_TL_dataset <- create_TL_dataset(testing(data_train_valid), train = FALSE)
# tf$data$experimental$cardinality(training_TL_dataset)$numpy()
# testing_TL_dataset <- testing(data_train_valid) %>%
# tensor_slices_dataset() %>%
# dataset_map(~.x %>% list_modify(
# # decode_jpeg yields a 3d tensor of shape (256, 256, 3)
# # Check tensor shapes!
# img = tf$image$decode_png(tf$io$read_file(.x$img), channels = 3),
# mask =tf$image$decode_png(tf$io$read_file(.x$img), channels = 3)
# ))
#
# example_TL <- training_TL_dataset %>% reticulate::as_iterator() %>% reticulate::iter_next()
# # example_TL
###### OLD plot_ly() interactive version
# rasterPlotly <- function (image, name="", hovermode = NULL) {
# myPlot <- plot_ly(type="image", z=image, name=name, hoverlabel=name, text=name,
# hovertext=name, hoverinfo="name+x+y") %>%
# layout(hovermode = hovermode, xaxis = list(hoverformat = '.1f'), yaxis = list(hoverformat = '.1f'))
# return(myPlot)
# }
#
# # Training Case
# img_and_mask <- training_TL_dataset %>%
# reticulate::as_iterator() %>% reticulate::iter_next() # shape=(batch=32, 256, 256, channel=3)
# img <- img_and_mask[[1]][1,,,1] %>% as.array() # 3-channel Input image
# target<- img_and_mask[[2]][1,,,1] %>% as.array() # 3-channel Output image
# p1 <- plot_ly(z=~255*img, type="heatmap", name = "Input Image") %>% hide_colorbar()
# p2 <- plot_ly(z=~255*target, type="heatmap", name = "Output Image") %>% hide_colorbar()
# subplot(p1,p2, shareY = TRUE) %>% layout(title="Training Case: 3-Channel Input Image (Left) & Output Image (Right)")
library(ggplot2)
library(patchwork)
# Get one batch from the transfer learning dataset
example_TL <- training_TL_dataset %>%
reticulate::as_iterator() %>%
reticulate::iter_next()
# Extract input image (first sample, first channel) and target (same structure)
img <- example_TL[[1]][1,,,1] %>% as.array() # Input image (first channel)
target <- example_TL[[2]][1,,,1] %>% as.array() # Output image (should equal input in this TL setup)
# Helper function to convert matrix to ggplot heatmap
mat_to_gg <- function(mat, title, multiply_255 = TRUE) {
if(multiply_255) mat <- 255 * mat
df <- expand.grid(x = 1:ncol(mat), y = 1:nrow(mat))
df$value <- as.vector(mat)
ggplot(df, aes(x = x, y = y, fill = value)) +
geom_raster() +
scale_fill_gradient(low = "black", high = "white") +
coord_fixed() +
theme_void() +
theme(legend.position = "none",
plot.title = element_text(hjust = 0.5, size = 12)) +
labs(title = title)
}
# Create the two plots
p_input <- mat_to_gg(img, "Input Image", multiply_255 = TRUE)
p_output <- mat_to_gg(target, "Output Image", multiply_255 = TRUE)
# Combine side by side with a shared title
(p_input | p_output) +
plot_annotation(
title = "Training Case: 3‑Channel Input Image (Left) & Output Image (Right)",
theme = theme(plot.title = element_text(hjust = 0.5))
)# MODEL FITTING - uncomment this fragment to run 6-epochs (2+ hrs)
# modTransferLearning_History_6Epochs <- modTransferLearning %>% fit(training_TL_dataset, epochs = 6, validation_data = validation_TL_dataset, verbose = 2)
# 63/63 - 1009s - loss: 1.2179 - mean_absolute_error: 0.4255 - poisson: 1.1990 - kullback_leibler_divergence: 2.0313 - val_loss: 0.4258 - val_mean_absolute_error: 0.3001 - val_poisson: 1.0742 - val_kullback_leibler_divergence: 2.0180
# Save model HDF5
# save_model_hdf5(modTransferLearning, "C:/Users/Dinov/Desktop/model_TF_TL_SynthBrainImages_epoch_6.h5",
# overwrite = TRUE, include_optimizer = TRUE)
# Model is on Canvas: https://umich.instructure.com/courses/38100/files/folder/Case_Studies/36_TCGA_MRI_Segmentation_Data_Phenotypes
# mod3_TL <-
# load_model_hdf5("E:/Ivo.dir/Research/UMichigan/Proposals/2014/UMich_MINDS_DataSciInitiative_2014/2016/MIDAS_HSXXX_DataScienceAnalyticsCourse_Porposal/MOOC_Implementation/appendix/model_TF_TL_SynthBrainImages_epoch_6.h5", compile = FALSE)Once we have trained (history <- model %>% fit(())
and saved (save_model_hdf5()) the model
(model), we can load it back as mod1 the
interactive session and use it for prediction.
# mod3_TL <- modTransferLearning
# mod3_TL <- load_model_hdf5("E:/Ivo.dir/Research/UMichigan/Proposals/2014/UMich_MINDS_DataSciInitiative_2014/2016/MIDAS_HSXXX_DataScienceAnalyticsCourse_Porposal/MOOC_Implementation/appendix/model_TF_TL_SynthBrainImages_epoch_6.h5", compile = F)
# Load the h5 model from Canvas: https://umich.instructure.com/courses/38100/files/folder/Case_Studies/36_TCGA_MRI_Segmentation_Data_Phenotypes
mod3_TL <- load_model_hdf5(paste0(localModelsFolder,
"/appendix/model_TF_TL_SynthBrainImages_epoch_6.h5"), compile = FALSE)
# Recompile the model with our custom dice() loss function
mod3_TL %>% compile(
optimizer = optimizer_rmsprop(learning_rate = 1e-5),
# loss = 'loss_kullback_leibler_divergence',
loss = 'mse', # as we are looking at minimizing ||OrigImage - SynthImage||, i.e., the RMSE
metrics = list(metric_mean_absolute_error, metric_poisson)
)
# Load the performance metrics of pre-computed 6-epoch model(modTransferLearning_History_6Epochs_DF.csv)
modTransferLearning_History_6Epochs_DF <- read.csv("https://umich.instructure.com/files/22825191/download?download_frd=1", header = T)
# modTransferLearning_History_6Epoc_DF <- as.data.frame(modTransferLearning_History_6Epochs$metrics)
#
# modTransferLearning_History_6Epochs_DF <- data.frame(
# epoch = c(1:dim(modTransferLearning_History_6Epoc_DF)[1]),
# loss = modTransferLearning_History_6Epoc_DF$loss,
# mean_absolute_error=modTransferLearning_History_6Epoc_DF$mean_absolute_error,
# poisson=modTransferLearning_History_6Epoc_DF$poisson,
# val_loss = modTransferLearning_History_6Epoc_DF$val_loss,
# val_mean_absolute_error = modTransferLearning_History_6Epoc_DF$val_mean_absolute_error,
# val_poisson = modTransferLearning_History_6Epoc_DF$val_poisson)
head(modTransferLearning_History_6Epochs_DF)## X epoch loss mean_absolute_error poisson val_loss
## 1 1 1 0.06418122 0.14311932 1.006497 0.05342003
## 2 2 2 0.04373177 0.12279665 1.006611 0.03685201
## 3 3 3 0.03213732 0.10813147 1.017886 0.02786113
## 4 4 4 0.02621992 0.09721498 1.042131 0.02372373
## 5 5 5 0.02366107 0.08954988 1.059174 0.02232249
## 6 6 6 0.02273074 0.08592803 1.048274 0.02177707
## val_mean_absolute_error val_poisson
## 1 0.13215400 1.024515
## 2 0.11384638 1.023064
## 3 0.10080072 1.040739
## 4 0.09157515 1.067587
## 5 0.08643402 1.073721
## 6 0.08500605 1.057148
# write.csv(modTransferLearning_History_6Epochs_DF, "C:/Users/Dinov/Desktop/modTransferLearning_History_6Epochs_DF.csv")
# modTransferLearning_History_6Epochs_DF <- read.csv("https://umich.instructure.com/files/22825191/download?download_frd=1", header = T)
# modTransferLearning_History_4Epochs_DF <- read.csv("https://umich.instructure.com/files/22704304/download?download_frd=1", header = T)
# EVALUATION
plot_ly(data=modTransferLearning_History_6Epochs_DF, x = ~epoch,
y = ~loss, type = "scatter", mode="markers+lines", name="Loss") %>%
add_trace(x = ~epoch, y = ~mean_absolute_error, type = "scatter", mode="markers+lines", name="Mean Abs Error") %>%
add_trace(x = ~epoch, y = ~poisson, type = "scatter", mode="markers+lines", name="Poisson") %>%
add_trace(x = ~epoch, y = ~val_loss, type = "scatter", mode="markers+lines", name="Valid Loss") %>%
add_trace(x = ~epoch, y = ~val_mean_absolute_error, type = "scatter", mode="markers+lines", name="Valid MAE") %>%
add_trace(x = ~epoch, y = ~val_poisson, type = "scatter", mode="markers+lines", name="Valid Poisson") %>%
layout(title="Transfer-Learning Validation Synth Image Generation Performance", xaxis=list(title="epoch"),
yaxis=list(title="Metric Value"), legend = list(orientation='h'))# # mod3_TL %>% evaluate(training_TL_dataset %>% dataset_batch(25), verbose = 0)
# mod3_TL %>% evaluate(validation_TL_dataset, verbose = 0)
# # Epoch4 loss mean_absolute_error poisson kullback_leibler_divergence
# # 0.1845085 0.2612749 0.8180779 1.2870733
# # Epoch6 loss mean_absolute_error poisson
# # 0.02177707 0.08500605 1.05714786
#
# # TL Model Prediction using the default (prior to Transfer Learning) model
# batch <- validation_TL_dataset %>% reticulate::as_iterator() %>% reticulate::iter_next()
# predictions <- predict(mod3_TL, batch[[1]])
# images <- tibble(
# image = batch[[1]][,,,1] %>% array_branch(1),
# synth_img = predictions[,,,1] %>% array_branch(1),
# target = batch[[2]][,,,1] %>% array_branch(1)
# )
# Check performance of the Transfer-Learning model on one case (# 22)
###### OLD plot_ly() interactive graph
# i=22
# pl1 <- plot_ly(z = ~ 255*as.matrix(as.data.frame(images$image[i])[,1:256]), type="heatmap", name=paste0("Input Image ", i))
# pl2 <- plot_ly(z = ~ 255*as.matrix(as.data.frame(images$target[i])[,1:256]), type="heatmap", name=paste0("Target ", i))
# pl3 <- plot_ly(z = ~ 255*as.matrix(as.data.frame(images$synth_img[i])[,1:256]), type="heatmap", name=paste0("Synth Image ", i))
# subplot(pl1, pl2, pl3, nrows=1) %>% hide_colorbar()
#
# pl_list <- list()
# for (i in 1:10) {
# image = as.matrix(as.data.frame(images$image[i])[,1:256])
# synth_img = as.matrix(as.data.frame(images$synth_img[i])[,1:256])
# target = as.matrix(as.data.frame(images$target[i])[,1:256])
#
# p1 <- plot_ly(z=~255*image, type="heatmap", name=paste0("Image ", i))
# p2 <- plot_ly(z=~255*target, type="heatmap", name=paste0("Target=Image ", i))
# p3 <- plot_ly(z=~255*synth_img, type="heatmap", name=paste0("Synth Img ", i))
# pl_list[[i]] <- subplot(p1,p2, p3, nrows=1) %>% hide_colorbar() # %>%
# # layout(yaxis=list(scaleanchor = "x", scaleratio = 1), hovermode = "y unified")
# # layout(hovermode = "y unified")
# }
# pl_list %>%
# subplot(nrows = length(pl_list)) %>%
# layout(title="Input Brain Images, Targets, and Synth Reconstructions for N=10 Cases")
# ---- Evaluation (unchanged) ----
mod3_TL %>% evaluate(validation_TL_dataset, verbose = 0)## loss mean_absolute_error poisson
## 0.02244135 0.08668123 1.07857680
# ---- TL Model Prediction ----
batch <- validation_TL_dataset %>% reticulate::as_iterator() %>% reticulate::iter_next()
predictions <- predict(mod3_TL, batch[[1]])## 1/1 - 19s - 19s/epoch - 19s/step
images <- tibble(
image = batch[[1]][,,,1] %>% array_branch(1),
synth_img = predictions[,,,1] %>% array_branch(1),
target = batch[[2]][,,,1] %>% array_branch(1)
)
# ---- Helper function to convert matrix to ggplot heatmap ----
mat_to_gg <- function(mat, title, multiply_255 = TRUE) {
if(multiply_255) mat <- 255 * mat
df <- expand.grid(x = 1:ncol(mat), y = 1:nrow(mat))
df$value <- as.vector(mat)
ggplot(df, aes(x = x, y = y, fill = value)) +
geom_raster() +
scale_fill_gradient(low = "black", high = "white") +
coord_fixed() +
theme_void() +
theme(legend.position = "none",
plot.title = element_text(hjust = 0.5, size = 10)) +
labs(title = title)
}
# ---- Single case (i = 22) ----
i <- 22
img_mat <- as.matrix(as.data.frame(images$image[i])[,1:256])
target_mat <- as.matrix(as.data.frame(images$target[i])[,1:256])
synth_mat <- as.matrix(as.data.frame(images$synth_img[i])[,1:256])
p_img <- mat_to_gg(img_mat, paste0("Input Image ", i))
p_target <- mat_to_gg(target_mat, paste0("Target ", i))
p_synth <- mat_to_gg(synth_mat, paste0("Synth Image ", i))
single_plot <- (p_img | p_target | p_synth) +
plot_annotation(title = paste("Transfer Learning – Case", i),
theme = theme(plot.title = element_text(hjust = 0.5)))
print(single_plot)# ---- Multi‑case (10 cases) – static faceted version ----
all_cases <- map_dfr(1:10, function(i) {
img_mat <- as.matrix(as.data.frame(images$image[i])[,1:256])
target_mat <- as.matrix(as.data.frame(images$target[i])[,1:256])
synth_mat <- as.matrix(as.data.frame(images$synth_img[i])[,1:256])
bind_rows(
expand.grid(x = 1:ncol(img_mat), y = 1:nrow(img_mat)) %>%
mutate(value = as.vector(255 * img_mat),
case = paste0("Case ", i),
type = "Input Image"),
expand.grid(x = 1:ncol(target_mat), y = 1:nrow(target_mat)) %>%
mutate(value = as.vector(255 * target_mat),
case = paste0("Case ", i),
type = "Target (Input)"),
expand.grid(x = 1:ncol(synth_mat), y = 1:nrow(synth_mat)) %>%
mutate(value = as.vector(255 * synth_mat),
case = paste0("Case ", i),
type = "Synthesised Image")
)
})
ggplot(all_cases, aes(x = x, y = y, fill = value)) +
geom_raster() +
facet_grid(case ~ type) +
scale_y_reverse() +
scale_fill_gradient(low = "black", high = "white") +
coord_fixed() +
theme_void() +
theme(legend.position = "none",
strip.text = element_text(size = 10, face = "bold")) +
labs(title = "Input Brain Images, Targets, and Synth Reconstructions for N=10 Cases")The preprocessing pipeline allows inspecting intermediate results
using reticulate::as_iterator() on the dataset.
# library(reticulate)
# example <- training_dataset %>% reticulate::as_iterator() %>% reticulate::iter_next()
# ex_img <- example$img %>% as.array() # %>% as.raster()
# plot_ly(type="image", z=255*ex_img) # 255*EBImage::Image(t(ex_img)))
# display one brain image
batch <- training_dataset %>% reticulate::as_iterator()
# pl_list <- list()
# for (i in 1:10) {
# record <- reticulate::iter_next(batch)
# image = record[[1]] %>% as.array()
# mask = record[[2]] %>% as.array()
# p1 <- plot_ly(z=~image[i,,,3], type="heatmap", name=paste0("Image ", i))
# p2 <- plot_ly(z=~mask[i,,,1], type="heatmap", name=paste0("Mask ", i))
# pl_list[[i]] <- subplot(p1,p2, nrows=1) %>% hide_colorbar() # %>%
# # layout(yaxis=list(scaleanchor = "x", scaleratio = 1), hovermode = "y unified")
# # layout(hovermode = "y unified")
# }
# pl_list %>%
# subplot(nrows = length(pl_list)) %>%
# layout(title="Brain Images and Masks for N=10 Cases")
batch <- training_dataset %>% reticulate::as_iterator()
all_pairs <- map_dfr(1:10, function(i) {
record <- reticulate::iter_next(batch)
img_mat <- record[[1]] %>% as.array() # shape (32,256,256,3) – we take first sample
mask_mat <- record[[2]] %>% as.array()
# Use first image in batch, first channel
img_slice <- (1.3)*img_mat[1,,,1]
mask_slice <- mask_mat[1,,,1]
bind_rows(
expand.grid(x = 1:ncol(img_slice), y = 1:nrow(img_slice)) %>%
mutate(value = as.vector(img_slice),
case = paste0("Case ", i),
type = "Brain Image"),
expand.grid(x = 1:ncol(mask_slice), y = 1:nrow(mask_slice)) %>%
mutate(value = as.vector(mask_slice),
case = paste0("Case ", i),
type = "Mask")
)
})
ggplot(all_pairs, aes(x = x, y = y, fill = value)) +
geom_raster() +
facet_grid(case ~ type) +
scale_y_reverse() +
scale_fill_gradient(low = "black", high = "white") +
coord_fixed() +
theme_void() +
theme(legend.position = "none",
strip.text = element_text(size = 10, face = "bold")) +
labs(title = "Brain Images and Masks for N=10 Cases")For the subsequent transfer-learning, we need to add additional
layers to the base-model (mod1). The output of every
Conv2D and MaxPooling2D layer is a 3D tensor
of shape (height, width, channels). In our-case, the final
mask-output is a single-channel image. The width and
height dimensions typically shrink with network layer depth.
The number of output channels for each Conv2D layer is
controlled by the filters parameter (e.g., 32 or 64). As
the width and height shrink, we can add more output channels in each
Conv2D layer in the NN.
Note that the new transfer-learning model (TL_model) has
only \(17K\) parameters to estimate, as
the \(31M\) parameters of the base
model (mod) are now frozen, i.e., they will not be tuned or
estimated during the TL_model re-fitting, which will be
much faster than the estimation of the original model.
After we design the DCNN model (Unet), we need to again compile it and estimate the fit to obtain the remaining trainable parameters.
Transforming 2D or higher-dimensional images as feature-vectors disregard some of the spatial interaction between pixels, voxels, and tensors. Convolution layers tend to capture and protect some of the spatial information from neighboring spatial locations. This is accomplished by down-sampling the image into features by convolving the images with kernels (filters) and then using the resampled convolution images to predict specific outcomes (images, values, classes, etc.) The use of multiple convolution kernels to “filter” the image involves computing a product to extract different features from the images.
For an image \(f(m,n)\), and a kernel \(g(k,l)\) defined over an integer grid \(\{m,n,k,l\in \mathbb{Z}\}\), the discrete convolution of \(f\) and \(g\) is:
\[\underbrace{(f*g)}_{convolution}[m,n]=\sum_{m=-\infty}^{\infty} {\sum_{n=-\infty}^{\infty} {\left (f[k, l]\times g[m-k, n-l]\right )}},\] where typically, the support of \(f\) and \(g\) is compact, e.g., \(0\leq m,k\leq M-1\) and \(0\leq n,l\leq N-1\).
The convolution of two finite sequences is defined by extending the sequences to finitely supported functions on the set of integers. When the sequences are the coefficients of two polynomials, then the coefficients of the ordinary product of the two polynomials are the convolution of the original two sequences. This is known as the Cauchy product of the coefficients of the sequences.
For instance, edge detection in an image can be done using a Sobel kernel matrix for vertical (\(y\)) and horizontal (\(x\)) edges.
\[{\displaystyle g_{x}={\begin{bmatrix}+1&0&-1\\+2&0&-2\\+1&0&-1\end{bmatrix}} \quad {\mbox{and}}\quad g_{y}={\begin{bmatrix}+1&+2&+1\\0&0&0\\-1&-2&-1\end{bmatrix}}}.\]
## [,1] [,2] [,3]
## [1,] 1 0 -1
## [2,] 2 0 -2
## [3,] 1 0 -1
## [,1] [,2] [,3]
## [1,] 1 2 1
## [2,] 0 0 0
## [3,] -1 -2 -1
library(jpeg)
library(magick)
img_url <- "https://umich.instructure.com/files/1627149/download?download_frd=1"
f <- image_read(img_url)
plot(f)# To apply the convolution process manually, we use a convolve() function of the 'magick' package.
imgX <- image_convolve(f, sobelX)
imgY <- image_convolve(f, sobelY)
# plot(imgX, imgY)
# Rotate 90 degrees
F <- imager::mirror(imager::imrotate(imager::magick2cimg(f), 90), "x")
ImgX <- imager::mirror(imager::imrotate(imager::magick2cimg(imgX), 90), "x")
ImgY <- imager::mirror(imager::imrotate(imager::magick2cimg(imgY), 90), "x")
p1 <- plot_ly(z=~255*(ImgX)[,,1,], type="image", name="(f*SobelX)")
p2 <- plot_ly(z=~255*(F)[,,1,], type="image", name="f")
p3 <- plot_ly(z=~255*(ImgY)[,,1,], type="image", name="(f*SobelY)")
subplot(p1, p2, p3, nrows=1) %>%
layout(title="Image convolution with Sobel Kernel Filters (Left=f*SobelX, Middle=f, Right=f*SobelY)")During the DCNN training process, the encoding phase typically
included 2D convolutional layers (layer_conv_2d()) paired
with pooling layers that reduce the size of the tensor shape and
transform images by sliding the kernel filter by a stride (1, 2, 3,
etc.) pixels to the right and down. The relation between the resulting
feature-vector size and the kernel size is represented by:
\[Feature\ size = \frac{Image\ size\ −\
Kernel\ size}{Stride} + 1.\] For instance, a 10x10 square image,
a filter of size 4x4, and a stride of 2 pixels, the \(Feature\ size = \frac{10 − 4}{2} + 1 = 3\).
Similarly, during the decoding phase, we include deconvolutional layers
(layer_conv_2d_transpose()) that reverse the process by
increasing the grid-size (tensor shape sizes) until we reach the desired
output layer shape, usually the same as the input images, but could also
be different. More information about tensorflow/keras
layers, loss
functions, and DCNN
model performance metrics is available on the RStudio
Tensorflow/Keras website.
Max Pooling Layer. Max pooling layers shrink the spatial extent of the convolved features and reduce overfitting by providing an abstracted feature representation. Instead of convolving (i.e., dot-product multiplying the image and the kernel) between the input and the kernel, Max-Pooling layers take the maximum value of image intensity over the region covered by the kernel filter. There are many alternatives to max-pooling, e.g., average-pooling, which computes the mean (arithmetic-average) of all image intensities covered by the kernel filter.
Fully Connected layers: Input nodes (from the left) in fully connected layers are connected to every node in the subsequent layer to the right. One or several fully connected layers may be common towards the end of a DCNN to provide support for learning non-linear affinities between high-level features generated as outputs of the prior convolutional layer. Good network designs typically include dropout layers between two consecutive fully connected layers (to reduce overfitting) and specify activation functions to capture non-linearity.
The final fully connected layer allows us to control the output tensor shape size which reflects the expected type of classification, prediction, or forecasting. For instance, if the outp[ut is expected to be 6-class labeling, the final layer will output a vector of size 6, i.e., one node for each possible class label. A softmax of this 6-feature vector would yield a 6D vector containing probabilities (\(0\leq p_i\leq 1\), \(\forall 1\leq i\leq 6\)) one for each class label. Dropout layers provide a mechanism for regularizing the model and reducing overfitting of the DCNN. Dropout layers may follow fully connected layers or appear after other max-pooling layers to generate image noise augmentation. Dropout layers randomly annihilate (set to zero) some of the connections of the input tensor, according to a Bernoulli distribution; hence some inputs are triaged with probability \(p\).
One can use tensorboard to dynamically track the
progression of an ongoing training process as well as to visualize a
neural network structure as a graph. See these examples for more
details, example
1 and example
2. The basic mechanism for this involves the following steps.
tensorboard (in a terminal/shell outside
R/RStudio): This can be done in different ways and is system/OS
dependent. Some examples include:
python -m pip install --user --upgrade pipconda install -c conda-forge tensorboardconda activate tensorflowtensorboard UI (from terminal shell):
> tensorboard --logdir logs/run_a. Alternatively, you
can launch tensorboard from RStudio directly via Rstudio –> Tools
–> Shell and entering this command in the shell
tensorboard --logdir logs/run_a.http://localhost:6006/print_dot_callback()) with printing periods
or this
example.This DSPA2 module represents Part 3 of the DSPA2 Chapter 14 (Deep Learning, Neural Networks). Learners are encouraged to first complete Part 1 of the DSPA2 Chapter 14 (Deep Learning, Neural Networks), Part 2 of the DSPA2 Chapter 14 (Deep Learning, Neural Networks), and Part 3 of the DSPA2 Chapter 14 (Deep Learning, Neural Networks) prior to continuing with this Part 4 of the DSPA2 Chapter 14 (Deep Learning, Neural Networks), which covers the Torch and Tensorflow Image Pre-processing Image Classification Pipelines.