---
title: "Class Activation Mapping in handwritten Bengali detection"
author: "Jesús Martín de la Sierra"
output:
    html_document:
        number_sections: yes
        highlight: tango
---

# Introduction

I’m an enthusiast of data visualization not only because of those beautiful charts that reveal trends, relations and patterns but also because we can use visualizations to show how the algorithms see and understand our reality from a different approach, learning from the data they process.

In this notebook I'll show a technique called **Gradient-weighted Class Activation Mapping** (or **Grad-CAM** for short) which is used to make convolutional neural network based-models more transparent by visualizing the image regions that are considered important for the different class predictions. Besides making CNN more explainable, this can be an interesting tool for debugging, fine-tuning and making architectures more robust or even more fair.

The data used for the demonstration is a set of Bengali handwritten scripts which have been previously trained by a CNN to detect their **vowel diacritics**. If you want to see how Bengali looks like, here you have an example that means *do you speak Bengali?*:

<p>
<center><font size="5">আপনি কি বাংলা বলতে পারেন?</font></center>
</p>

To know more about Bengali language and its writing system, please look at the references below.

# Data and model

I've used a modified data set respect to the original provided for the [Bengali.AI Handwritten Grapheme Classification](https://www.kaggle.com/c/bengaliai-cv19) competition. The modification implies a reframing and resizing of the Bengali scripts, so I got a set of 100000 scripts of 64x64 pixels with one single channel. 90000 samples were used for training while the remaining 10000 were for validation.

Despite the training set contains many other Bengali grapheme constituents, in this notebook I'm focusing only on the vowel diacritic detection. If you look at some of the references, you'll see that there exist 11 classes including the inexistent vowel diacritic. Here you can see the ten diacritics that have a written component:

<p>
<center><font size="5">া ি ী ু ূ ৃ ে ৈ ো ৌ </font></center>
</p>

In reference to the CNN model used, this takes input data tensors of shape (64, 64, 1), according to the input images. Then, convolution and pooling layers are added subsequently until a tensor of shape (16, 16, 64) is obtained. Finally, fully connected layers of 256, 192 and 11 nodes are added. Note that this latter layer is because of the number of classes (here including the null diacritic). You can see some more details about the architecture in the implementation section.

Remember that the model is already trained so I only use it in this notebook for inference.

# Grad-CAM

Without going into details, I'll explain briefly the operation of this visualization technique. In the implementation section you can see how it's computed step by step in R and Keras.

Grad-CAM consists in taking the last convolutional layer from the CNN architecture so we preserve the spatial information which is lost in the following fully connected layer(s). Here we obtain a set of feature maps each one activated in different ways by the input image. We also take the most likely output class (this could be another class if we'd like to see the regions of the input image that make important other predictions).

Then, we compute the gradient of the output class prediction (the class score or probability) with respect to each feature map. This gives us a set of gradients which are globally averaged. The resulting output is a vector of gradients, each one representing the mean intensity of the gradient for the specific feature map.

Once again, we compute the mean value for all the feature maps so we obtain a heatmap where positive and negative values represent importance factors of the input image. The positive values mean a majority of feature maps participating in the predicted class. Otherwise, the negative values represent the same for the other classes.

# Implementation

In this section is implemented the code for the Grad-CAM technique and I use it to visualize some examples. Before that, I also show the required code for package and data load, although maybe you won't find it much useful since I don't go into details. If you need to know more, you are welcome to ask me.

## Packages

```{r message=FALSE, warning=FALSE}
# Packages
library(keras)
library(imager)
library(viridis)
library(stringr)
```

## Input data and labels

```{r}
# Input data. 100000 128x128 images resized to 64x64 and converted to a tensor of shape (100000, 64, 64, 1).
load("../input/bengali-128x128-data/x_train.RData")

# Output classes
y_train <- read.csv2(file="../input/bengali-128x128-data/bengali_labels.csv", header=TRUE, sep=",")
y_train <- y_train[1:nrow(x_train),]

# One-hot encoding of vowel diacritic labels
y_train_vowel <- to_categorical(y_train$vowel_diacritic)
```

## Model

```{r}
# CNN model for vowel diacritic classification
model <- load_model_hdf5(filepath="../input/bengali-128x128-data/bengali_vowel_model.h5", compile=FALSE)

# CNN model details
summary(model)
```

## Grad-CAM

Here comes the Grad-CAM technique. A set of 18 script samples from the validation set is chosen such that they contain one of the following diacritics:

<p>
<center><font size="5">ি ু</font></center>
</p>

```{r fig.align="center", fig.width=5, fig.height=6}
# Setting 9 images by plot
par(mfrow=c(3, 3), mar=c(0.5, 0.25, 1, 0.25), oma=c(3, 0, 3, 0))

# Loop for the 18 chosen scripts in the validation set
for (i in c(90032, 90039, 90051, 90055, 90057, 90064, 90068, 90079, 90081, 90026, 90041, 90052, 90053, 90060, 90065, 90066, 90072, 90078)) {
    
    # For each index in the data set
    script_index <- i
    
    # Convert the script given by the index to a tensor of shape (1, 64, 64, 1)
    script <- array_reshape(x_train[script_index,,,], c(1, 64, 64, 1))
    
    # Prediction of the input image
    prediction <- model %>% predict(script)
    
    # Index of the most likely class
    class_index <- which.max(prediction)
    
    # Get the output of the model for that index
    vowel_class <- model$output[, class_index]

    # Last convolutional layer from the model
    last_conv_layer <- model %>% get_layer("conv2d_5")
    
    # Gradient of the predicted class with respect to the last conv. layer
    grads <- k_gradients(vowel_class, last_conv_layer$output)[[1]]

    # Global average of the gradients
    pooled_grads <- k_mean(grads, axis=c(1, 2, 3))

    # Function to access to the pooled_grads (weights) and output values from the last conv. layer given the input
    iterate <- k_function(list(model$input), list(pooled_grads, last_conv_layer$output[1,,,]))
    
    # Apply the previous function for the input image
    c(pooled_grads_value, conv_layer_output_value) %<-% iterate(list(script))

    # For each feature map from the last conv. layer we compute the weighted output value
    for (i in 1:64) {
        conv_layer_output_value[,,i] <- conv_layer_output_value[,,i] * pooled_grads_value[[i]]
    }
    
    # Compute the mean over the feature maps
    class_heatmap <- apply(conv_layer_output_value, c(1, 2), mean)
    
    # Convert to 0 all negative values since they represent importance factors for other classes
    class_heatmap[class_heatmap<0] <- 0
    
    # Convert script to normalized image
    script <- script[1,,,] %>% as.cimg() %>% renorm(0, 1)
    
    # Define palette for class activation heatmap
    colors <- viridis_pal(begin=0, end=1, option="inferno")(5)
    cscale <- scales::gradient_n_pal(colors, seq(0, 1, 1/4))
    
    # Convert heatmap to image. Resize to 64x64 for input size matching. Apply blur and palette.
    class_heatmap <- class_heatmap %>% as.cimg() %>% renorm(0, 1) %>% resize(64, 64) %>% isoblur(1) %>% as.raster(colourscale=cscale, rescale=FALSE) %>% as.cimg() %>% renorm(0, 1)
    
    # Overlap both input image and heatmap
    imdraw(script %>% add.color(), class_heatmap, opacity=0.7) %>% plot(main=paste0("p=", str_trunc(max(prediction), width=4, side="right", ellipsis="")), axes=FALSE, interpolate=FALSE)
    
}
```

Above we see two mosaics of 3x3 images each. Let's discuss them.

The first mosaic shows nine scripts with the ি diacritic. Note that there isn't a clear pattern for the detection of this mark: the CNN looks at the sorroundings rather than the script itself to identify the class. If we see in detail, four of the scripts have in common that the bottom termination of the diacritic stroke strongly participates in the identification. Other three scripts are correctly classified because of the central cross of the vowel component but also due to some other regions. Finally, the remaning two scripts are less clear, although maybe the top curved feature play the main role for the right classification. Despite this diversity of features involved, the classification is done with very high accuracy. However, from my point of view, this isn't very desirable since we might want an explainable model. In this case, we might prefer a model that looks at the same features to decide the class as the same way we humans do when reading.

The second mosaic shows nine scrips where the ু diacritic is present. At a glance, we see that the result is definitely different. The nine examples show a clear spot on the vowel mark and that's just what we'd look for if we wanted an explainable model. It gives confidence in the model since it looks at the same feature to decide the class. A side effect in this case is that the model also *finds interesting* other similar strokes as we can see in two of the examples, but this is rather about a known property of the CNNs: the translation invariance.

# References

https://www.kaggle.com/c/bengaliai-cv19

https://en.wikipedia.org/wiki/Bengali_alphabet

https://en.wikibooks.org/wiki/Bengali/Script/Diacritics

https://www.omniglot.com/language/phrases/bengali.php

https://arxiv.org/abs/1610.02391

http://gradcam.cloudcv.org/

https://www.machinecurve.com/index.php/2019/11/28/visualizing-keras-cnn-attention-grad-cam-class-activation-maps/

https://medium.com/@ninads79shukla/gradcam-73a752d368be

Chollet, F. and Allaire, J.J. *Deep Learning with R*, Manning Publications, New York, USA. 2018.

# Acknowledges

I'd like to thank to the authors of the following notebooks.

The first one provides a data set resized to 128x128 pixels. This was especially useful since I couldn't open parquet files at the moment of publication of my notebook. I also processed the images so I finally worked with sizes of 64x64 pixels because of memory and training time reasons.

https://www.kaggle.com/iafoss/image-preprocessing-128x128

The second notebook shows an interesting proposal of CNN model which I adapted to match my requirements.

https://www.kaggle.com/kaushal2896/bengali-graphemes-starter-eda-multi-output-cnn