{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Cell classification model interpretability with LIME \n\n**Keywords:  microscopy images, cell classification, EfficientNetV2, LIME, XAI**","metadata":{}},{"cell_type":"markdown","source":"The three categories of neuronal cells in this dataset, **shsy5y**, **astro** and **cort**,  are easy to classify. With or perhaps even without the aid of image processing  enhancements like CLAHE, it is possible to classify  the 520x704 resolution images [visually](#visualization). It therefore stands to reason that a good  deep learning model would also be able to easily classify the images.  \nIt would be interesting to see what features a trained deep learning model uses to identify the classes. To this end we can apply LIME [[1]](#ref1), a post hoc explainability technique that makes individual predictions of a black box model like a deep neural net, locally interpretable. More on LIME in [Part III](#PartIII).\n\nIn this exercise, we will train a deep learning EfficientNetV2 model to classify the neuronal cell images. We will then apply LIME on each class of image to get an insight into the model. ","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd \n\nimport seaborn as sns\nimport matplotlib.pyplot as plt\nimport cv2   # for CLAHE\n\nimport os\nimport random \nimport glob\nfrom itertools import product  # for confusion matrix plot\nfrom itertools import islice\nimport time\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import confusion_matrix \nfrom sklearn.metrics import classification_report\n\nimport tensorflow as tf\nimport tensorflow_hub as hub\nfrom tensorflow import keras\nfrom tensorflow.keras.models import Sequential, Model, load_model\nfrom tensorflow.keras.layers import Dense, Dropout, BatchNormalization, Activation\nfrom tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping, ReduceLROnPlateau\n\n# for LIME\nfrom lime import lime_image\nexplainer = lime_image.LimeImageExplainer(random_state=2021)\nfrom skimage.segmentation import mark_boundaries\n\nimport warnings\nwarnings.simplefilter(\"ignore\")\n\nprint(\"TF version: \", tf.__version__)\nprint('Hub version:', hub.__version__)","metadata":{"execution":{"iopub.status.busy":"2022-01-03T06:03:29.242902Z","iopub.execute_input":"2022-01-03T06:03:29.24314Z","iopub.status.idle":"2022-01-03T06:03:36.324424Z","shell.execute_reply.started":"2022-01-03T06:03:29.243063Z","shell.execute_reply":"2022-01-03T06:03:36.323668Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### Path for EfficientNet V2 model weights","metadata":{}},{"cell_type":"code","source":"## Mapping copied from a \"TF Hub for TF2: Retraining an image classifier\" colab example\n## For efficientnet class of models\n\ndef get_hub_url_and_isize(model_name, ckpt_type, hub_type):\n  if ckpt_type == '1k':\n    ckpt_type = ''  # json doesn't support empty string\n  else:\n    ckpt_type = '-' + ckpt_type  # add '-' as prefix\n  \n  hub_url_map = {\n    'efficientnetv2-b0': f'gs://cloud-tpu-checkpoints/efficientnet/v2/hub/efficientnetv2-b0/{hub_type}',\n    'efficientnetv2-b1': f'gs://cloud-tpu-checkpoints/efficientnet/v2/hub/efficientnetv2-b1/{hub_type}',\n    'efficientnetv2-b2': f'gs://cloud-tpu-checkpoints/efficientnet/v2/hub/efficientnetv2-b2/{hub_type}',\n    'efficientnetv2-b3': f'gs://cloud-tpu-checkpoints/efficientnet/v2/hub/efficientnetv2-b3/{hub_type}',\n    'efficientnetv2-s':  f'gs://cloud-tpu-checkpoints/efficientnet/v2/hub/efficientnetv2-s/{hub_type}',\n    'efficientnetv2-m':  f'gs://cloud-tpu-checkpoints/efficientnet/v2/hub/efficientnetv2-m/{hub_type}',\n    'efficientnetv2-l':  f'gs://cloud-tpu-checkpoints/efficientnet/v2/hub/efficientnetv2-l/{hub_type}',\n\n    'efficientnetv2-b0-21k': f'gs://cloud-tpu-checkpoints/efficientnet/v2/hub/efficientnetv2-b0-21k/{hub_type}',\n    'efficientnetv2-b1-21k': f'gs://cloud-tpu-checkpoints/efficientnet/v2/hub/efficientnetv2-b1-21k/{hub_type}',\n    'efficientnetv2-b2-21k': f'gs://cloud-tpu-checkpoints/efficientnet/v2/hub/efficientnetv2-b2-21k/{hub_type}',\n    'efficientnetv2-b3-21k': f'gs://cloud-tpu-checkpoints/efficientnet/v2/hub/efficientnetv2-b3-21k/{hub_type}',\n    'efficientnetv2-s-21k':  f'gs://cloud-tpu-checkpoints/efficientnet/v2/hub/efficientnetv2-s-21k/{hub_type}',\n    'efficientnetv2-m-21k':  f'gs://cloud-tpu-checkpoints/efficientnet/v2/hub/efficientnetv2-m-21k/{hub_type}',\n    'efficientnetv2-l-21k':  f'gs://cloud-tpu-checkpoints/efficientnet/v2/hub/efficientnetv2-l-21k/{hub_type}',\n    'efficientnetv2-xl-21k':  f'gs://cloud-tpu-checkpoints/efficientnet/v2/hub/efficientnetv2-xl-21k/{hub_type}',\n\n    'efficientnetv2-b0-21k-ft1k': f'gs://cloud-tpu-checkpoints/efficientnet/v2/hub/efficientnetv2-b0-21k-ft1k/{hub_type}',\n    'efficientnetv2-b1-21k-ft1k': f'gs://cloud-tpu-checkpoints/efficientnet/v2/hub/efficientnetv2-b1-21k-ft1k/{hub_type}',\n    'efficientnetv2-b2-21k-ft1k': f'gs://cloud-tpu-checkpoints/efficientnet/v2/hub/efficientnetv2-b2-21k-ft1k/{hub_type}',\n    'efficientnetv2-b3-21k-ft1k': f'gs://cloud-tpu-checkpoints/efficientnet/v2/hub/efficientnetv2-b3-21k-ft1k/{hub_type}',\n    'efficientnetv2-s-21k-ft1k':  f'gs://cloud-tpu-checkpoints/efficientnet/v2/hub/efficientnetv2-s-21k-ft1k/{hub_type}',\n    'efficientnetv2-m-21k-ft1k':  f'gs://cloud-tpu-checkpoints/efficientnet/v2/hub/efficientnetv2-m-21k-ft1k/{hub_type}',\n    'efficientnetv2-l-21k-ft1k':  f'gs://cloud-tpu-checkpoints/efficientnet/v2/hub/efficientnetv2-l-21k-ft1k/{hub_type}',\n    'efficientnetv2-xl-21k-ft1k':  f'gs://cloud-tpu-checkpoints/efficientnet/v2/hub/efficientnetv2-xl-21k-ft1k/{hub_type}',\n      \n    # efficientnetv1\n    'efficientnet_b0': f'https://tfhub.dev/tensorflow/efficientnet/b0/{hub_type}/1',\n    'efficientnet_b1': f'https://tfhub.dev/tensorflow/efficientnet/b1/{hub_type}/1',\n    'efficientnet_b2': f'https://tfhub.dev/tensorflow/efficientnet/b2/{hub_type}/1',\n    'efficientnet_b3': f'https://tfhub.dev/tensorflow/efficientnet/b3/{hub_type}/1',\n    'efficientnet_b4': f'https://tfhub.dev/tensorflow/efficientnet/b4/{hub_type}/1',\n    'efficientnet_b5': f'https://tfhub.dev/tensorflow/efficientnet/b5/{hub_type}/1',\n    'efficientnet_b6': f'https://tfhub.dev/tensorflow/efficientnet/b6/{hub_type}/1',\n    'efficientnet_b7': f'https://tfhub.dev/tensorflow/efficientnet/b7/{hub_type}/1',\n  }\n  \n  image_size_map = {\n    'efficientnetv2-b0': 224,\n    'efficientnetv2-b1': 240,\n    'efficientnetv2-b2': 260,\n    'efficientnetv2-b3': 300,\n    'efficientnetv2-s':  384,\n    'efficientnetv2-m':  480,\n    'efficientnetv2-l':  480,\n    'efficientnetv2-xl':  512,\n  \n    'efficientnet_b0': 224,\n    'efficientnet_b1': 240,\n    'efficientnet_b2': 260,\n    'efficientnet_b3': 300,\n    'efficientnet_b4': 380,\n    'efficientnet_b5': 456,\n    'efficientnet_b6': 528,\n    'efficientnet_b7': 600,\n  }\n  \n\n  hub_url = hub_url_map.get(model_name + ckpt_type)\n  image_size = image_size_map.get(model_name)\n  return hub_url, image_size","metadata":{"execution":{"iopub.status.busy":"2022-01-03T06:03:36.326107Z","iopub.execute_input":"2022-01-03T06:03:36.326389Z","iopub.status.idle":"2022-01-03T06:03:36.341093Z","shell.execute_reply.started":"2022-01-03T06:03:36.326353Z","shell.execute_reply":"2022-01-03T06:03:36.337289Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"CONFIG = dict (\n    seed = 2021,    \n    num_classes = 3,\n    train_val_split = 0.2,\n    batch_size = 8,\n    epochs = 3, \n    learning_rate = 1e-5,\n    loss = 'sparse_categorical_crossentropy',\n    metrics = 'sparse_categorical_accuracy',\n    optimizer = tf.keras.optimizers.Adam(lr = 1e-4),\n)\n\n\n# Config for pretrained model \nCONFIG['model_type'] = 'efficientnetv2-b0'\nCONFIG['ckpt_type'] = '1k'   # '1k', '21k-ft1k', '21k'\nCONFIG['hub_type'] = 'feature-vector' \nhub_url, image_size = get_hub_url_and_isize(CONFIG['model_type'], CONFIG['ckpt_type'], CONFIG['hub_type'])\nprint(f'Hub URL: {hub_url}')\nCONFIG['img_width'] = image_size\nCONFIG['img_height'] = image_size\nCONFIG['img_size'] = image_size\nCONFIG['do_fine_tuning'] = True\n\n\n\ndef seed_everything(SEED):\n    np.random.seed(SEED)\n    tf.random.set_seed(SEED)\n    os.environ['PYTHONHASHSEED'] = str(SEED)\n    #os.environ['TF_CUDNN_DETERMINISTIC'] = str(SEED)\n    \nseed_everything(CONFIG['seed'])\n\nCSV_FILE = '../input/sartorius-cell-instance-segmentation/train.csv'\nTRAIN_PATH = '../input/sartorius-cell-instance-segmentation/train/'\n\nAUTOTUNE = tf.data.experimental.AUTOTUNE\n\nTRAIN_MODEL = False # if False, use saved model for LIME; if true, train a new model","metadata":{"execution":{"iopub.status.busy":"2022-01-03T06:03:36.343219Z","iopub.execute_input":"2022-01-03T06:03:36.343522Z","iopub.status.idle":"2022-01-03T06:03:36.379766Z","shell.execute_reply.started":"2022-01-03T06:03:36.34348Z","shell.execute_reply":"2022-01-03T06:03:36.379029Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Part I: EDA, data preprocessing (tf.data)","metadata":{}},{"cell_type":"code","source":"col_list = ['id', 'cell_type']\ndf = pd.read_csv(CSV_FILE, usecols=col_list)\ndf['path'] = TRAIN_PATH + df['id'] + '.png'\n\ndf.rename(columns = {\"cell_type\": \"label\"}, inplace = True)\nprint(f'The dataset has {df.shape[0]} entries.\\n')\n\nCLASSES = df['label'].unique()\nprint(f'The labels are: {CLASSES[0]}, {CLASSES[1]}, {CLASSES[2]}')","metadata":{"execution":{"iopub.status.busy":"2022-01-03T06:03:36.381765Z","iopub.execute_input":"2022-01-03T06:03:36.382409Z","iopub.status.idle":"2022-01-03T06:03:36.786634Z","shell.execute_reply.started":"2022-01-03T06:03:36.38237Z","shell.execute_reply":"2022-01-03T06:03:36.785914Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# dict to encode labels\nlabel_dict = {'shsy5y': 0, 'astro': 1, 'cort':2}\ndf['label'] = df['label'].replace(label_dict)\n\n# reverse dict to get back original labels\nrev_label_dict = dict((v,k) for k,v in label_dict.items())\n\ndf.head()","metadata":{"execution":{"iopub.status.busy":"2022-01-03T06:03:36.788874Z","iopub.execute_input":"2022-01-03T06:03:36.789141Z","iopub.status.idle":"2022-01-03T06:03:36.845438Z","shell.execute_reply.started":"2022-01-03T06:03:36.789106Z","shell.execute_reply":"2022-01-03T06:03:36.844588Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### Visualize each image class","metadata":{}},{"cell_type":"code","source":"plt.figure(figsize=(16, 12))\nfor i in range(3):\n    # get first id instance for each label\n    # the image id's are: '0030fd0e6378', '0140b3c8f445', '01ae5a43a2ab'\n    df_temp_ = df.loc[df.label == i].head(1)\n    filename = df_temp_.iloc[0,2] # get path+filename\n    \n    # CLAHE to enhance image\n    img = cv2.imread(filename)[..., 0]\n    clahe = cv2.createCLAHE(clipLimit=4.0, tileGridSize=(8, 8))\n    img2 = clahe.apply(img)\n    \n    ax = plt.subplot(1, 3, i + 1)\n    plt.imshow(img2, cmap='hsv')\n    plt.title(rev_label_dict.get(i))\n    plt.axis('off')\n    plt.tight_layout();","metadata":{"execution":{"iopub.status.busy":"2022-01-03T06:03:36.846698Z","iopub.execute_input":"2022-01-03T06:03:36.847091Z","iopub.status.idle":"2022-01-03T06:03:37.564783Z","shell.execute_reply.started":"2022-01-03T06:03:36.84705Z","shell.execute_reply":"2022-01-03T06:03:37.564112Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<a id='visualization'></a>","metadata":{}},{"cell_type":"markdown","source":"Based on the spacing and pattern, it is possible to visually identify an *astro* image from a *shsy5y* image from a *cort* image.\nThis is a descriptive summary of the three images:   \n- *shsy5y* cells: numerous, dense and of uniform size\n- *astro* cells: sparse with elongated processes that radiate from the center\n- *cort* cells: clusters of larger cells with smaller cells/artifacts in the background","metadata":{}},{"cell_type":"code","source":"X_train, X_valid = train_test_split(df, \n                                    test_size=CONFIG['train_val_split'], \n                                    random_state=CONFIG['seed'], \n                                    shuffle=True)\n\n\ntrain_ds = tf.data.Dataset.from_tensor_slices((X_train.path.values, X_train.label.values))\nvalid_ds = tf.data.Dataset.from_tensor_slices((X_valid.path.values, X_valid.label.values))\n\n# print path/label\nfor path, label in train_ds.take(3):\n    print ('Path: {}, Label: {}'.format(path, label))","metadata":{"execution":{"iopub.status.busy":"2022-01-03T06:03:37.565779Z","iopub.execute_input":"2022-01-03T06:03:37.56742Z","iopub.status.idle":"2022-01-03T06:03:39.815468Z","shell.execute_reply.started":"2022-01-03T06:03:37.567375Z","shell.execute_reply":"2022-01-03T06:03:39.813128Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def preprocess_data_train(image_path, label):\n    # load the raw data as a string\n    img = tf.io.read_file(image_path)\n    img = tf.image.decode_png(img, channels=3)\n    img = tf.cast(img, tf.float32) / 255.0 \n    # augment data in CPU\n    img = tf.image.random_brightness(img, 0.3)\n    img = tf.image.random_flip_left_right(img, seed=None)\n    img = tf.image.random_flip_up_down(img)\n    img = tf.image.resize(img, [CONFIG['img_size'],CONFIG['img_size']])\n    return img, label\n\ndef preprocess_data_valid(image_path, label):\n    img = tf.io.read_file(image_path)\n    img = tf.image.decode_png(img, channels=3)\n    img = tf.cast(img, tf.float32) / 255.0\n    img = tf.image.resize(img, [CONFIG['img_size'],CONFIG['img_size']])\n    return img, label\n\n\n# set num_parallel_calls to process multiple images in parallel\ntrain_ds = train_ds.map(preprocess_data_train, num_parallel_calls=AUTOTUNE)\nvalid_ds = valid_ds.map(preprocess_data_valid, num_parallel_calls=AUTOTUNE)","metadata":{"execution":{"iopub.status.busy":"2022-01-03T06:03:39.816773Z","iopub.execute_input":"2022-01-03T06:03:39.817543Z","iopub.status.idle":"2022-01-03T06:03:40.053077Z","shell.execute_reply.started":"2022-01-03T06:03:39.817505Z","shell.execute_reply":"2022-01-03T06:03:40.052358Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def configure_for_performance(ds, batch_size = 8):    \n    ds = ds.shuffle(buffer_size=1024)\n    ds = ds.batch(batch_size)\n    ds = ds.prefetch(buffer_size=AUTOTUNE)\n    return ds\n\n\ntrain_ds_batch = configure_for_performance(train_ds, CONFIG['batch_size'])\nvalid_ds_batch = valid_ds.batch(CONFIG['batch_size'])","metadata":{"execution":{"iopub.status.busy":"2022-01-03T06:03:40.05514Z","iopub.execute_input":"2022-01-03T06:03:40.055661Z","iopub.status.idle":"2022-01-03T06:03:40.076385Z","shell.execute_reply.started":"2022-01-03T06:03:40.055621Z","shell.execute_reply":"2022-01-03T06:03:40.07577Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### Plot augmented images\nA sanity check on the image pipeline where the images have been loaded, preprocessed (resized), batched and augmented (brightness, vertical/horizontal flips).","metadata":{}},{"cell_type":"code","source":"image_batch, label_batch = next(iter(train_ds_batch))\n\nplt.figure(figsize=(16, 12))\nfor i in range(6):\n    ax = plt.subplot(2, 3, i + 1)\n    plt.imshow(image_batch[i].numpy(), cmap='bone_r')\n    label = label_batch[i].numpy()\n    plt.title(rev_label_dict.get(label))\n    plt.axis(\"off\");","metadata":{"execution":{"iopub.status.busy":"2022-01-03T06:03:40.077845Z","iopub.execute_input":"2022-01-03T06:03:40.078344Z","iopub.status.idle":"2022-01-03T06:03:49.828706Z","shell.execute_reply.started":"2022-01-03T06:03:40.078307Z","shell.execute_reply":"2022-01-03T06:03:49.82804Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Part II: Model training","metadata":{}},{"cell_type":"code","source":"base_model = hub.KerasLayer(hub_url, trainable=True,\n                           input_shape=(CONFIG['img_width'], CONFIG['img_height'], 3))\n\nmodel = tf.keras.Sequential([\n    base_model,\n    tf.keras.layers.Dense(256, activation='relu'),\n    tf.keras.layers.Dropout(rate=0.3),\n    tf.keras.layers.Dense(CONFIG['num_classes'], activation='softmax')\n])\n\n\nmodel.compile(\n    optimizer=CONFIG['optimizer'],\n    loss=CONFIG['loss'],\n    metrics=CONFIG['metrics'])\nmodel.summary()","metadata":{"execution":{"iopub.status.busy":"2022-01-03T06:03:49.831288Z","iopub.execute_input":"2022-01-03T06:03:49.831992Z","iopub.status.idle":"2022-01-03T06:03:58.022691Z","shell.execute_reply.started":"2022-01-03T06:03:49.831953Z","shell.execute_reply":"2022-01-03T06:03:58.021984Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"weight_path_save = 'best_model.hdf5'\nlast_weight_path = 'last_model.hdf5'\n\ncheckpoint = ModelCheckpoint(weight_path_save, \n                             monitor= 'val_loss', \n                             verbose=1, \n                             save_best_only=True, \n                             mode= 'min', \n                             save_weights_only = False)\ncheckpoint_last = ModelCheckpoint(last_weight_path, \n                             monitor= 'val_loss', \n                             verbose=1, \n                             save_best_only=False, \n                             mode= 'min', \n                             save_weights_only = False)\n\n\nearly = EarlyStopping(monitor= 'val_loss', \n                      mode= 'min', \n                      patience=5)\n\nreduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.8, \n                                   patience=2, verbose=1, mode='auto', \n                                   epsilon=0.0001, cooldown=5, min_lr=0.00001)\ncallbacks_list = [checkpoint, checkpoint_last, early, reduce_lr]","metadata":{"execution":{"iopub.status.busy":"2022-01-03T06:03:58.024058Z","iopub.execute_input":"2022-01-03T06:03:58.024315Z","iopub.status.idle":"2022-01-03T06:03:58.031471Z","shell.execute_reply.started":"2022-01-03T06:03:58.024281Z","shell.execute_reply":"2022-01-03T06:03:58.030783Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# plot train and validation curves\n\ndef plot_hist(history):\n    loss = history.history['loss']\n    val_loss = history.history['val_loss']\n    acc = history.history['sparse_categorical_accuracy']\n    val_acc = history.history['val_sparse_categorical_accuracy']\n    epochs = range(1,len(loss)+1)\n\n    fig = plt.figure(figsize=(9, 5))\n    \n    plt.subplot(1, 2, 1)\n    plt.plot(epochs, loss, linestyle='--', linewidth=3, color='red', alpha=0.7, label='Train Loss')\n    plt.plot(epochs, val_loss, linestyle='dashdot', linewidth=2, color='green', alpha=0.8, label='Valid Loss')\n    plt.xlim(1, max(plt.xlim()))\n    plt.ylim(0,max(max(plt.ylim()), 0.01))\n    plt.xlabel('Epochs', fontsize=11)\n    plt.ylabel('Loss', fontsize=12)\n    plt.title('Training/Validation Loss')\n    plt.legend(fontsize=12)\n\n\n    plt.subplot(1, 2, 2) \n    plt.plot(epochs, acc, linestyle='--', linewidth=3, color='red', alpha=0.7, label='Train Acc')\n    plt.plot(epochs, val_acc, linestyle='solid', linewidth=4, color='green', alpha=0.8, label='Valid Acc') \n    plt.xlim(1, max(plt.xlim()))\n    plt.ylim(min(min(plt.ylim()), 0.98),1)\n    plt.xlabel('Epochs', fontsize=11)\n    plt.ylabel('Accuracy', fontsize=12)\n    plt.title('Training/Validation Accuracy')\n    plt.legend(fontsize=12)\n    \n    plt.tight_layout()\n    plt.show()","metadata":{"execution":{"iopub.status.busy":"2022-01-03T06:03:58.03317Z","iopub.execute_input":"2022-01-03T06:03:58.033743Z","iopub.status.idle":"2022-01-03T06:03:58.046492Z","shell.execute_reply.started":"2022-01-03T06:03:58.033707Z","shell.execute_reply":"2022-01-03T06:03:58.045847Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"if TRAIN_MODEL:\n    history = model.fit(train_ds_batch, \n                        validation_data = valid_ds_batch, \n                        epochs = CONFIG['epochs'], \n                        callbacks = callbacks_list,\n                       )\n    plot_hist(history)","metadata":{"execution":{"iopub.status.busy":"2022-01-03T06:03:58.047467Z","iopub.execute_input":"2022-01-03T06:03:58.048019Z","iopub.status.idle":"2022-01-03T06:03:58.058797Z","shell.execute_reply.started":"2022-01-03T06:03:58.047979Z","shell.execute_reply":"2022-01-03T06:03:58.057971Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Part III: Explainability with LIME","metadata":{}},{"cell_type":"markdown","source":"### Evaluating Model on Validation Set","metadata":{}},{"cell_type":"code","source":"def plot_confusion_matrix(cm, \n                          classes,\n                          title='Confusion matrix',\n                          cmap=plt.cm.Purples):\n   \n\n    accuracy = np.trace(cm) / float(np.sum(cm))\n    misclass = 1 - accuracy  \n    \n    plt.figure(figsize=(8, 6))\n    plt.imshow(cm, interpolation='nearest', cmap=cmap)\n    \n    tick_marks = np.arange(len(classes))\n    plt.xticks(tick_marks, classes, rotation=45, fontsize=15)\n    plt.yticks(tick_marks, classes, fontsize=15)\n    \n    plt.xlabel('Predicted label\\naccuracy={:0.4f}; misclass={:0.4f}'\n           .format(accuracy, misclass),fontsize=15)\n    plt.ylabel('True label', fontsize=15)\n    plt.title(title, fontsize=22);\n    plt.colorbar()\n\n    thresh = cm.max() / 2.0\n    for i, j in product(range(cm.shape[0]), range(cm.shape[1])):\n        plt.text(j, i, \"{:,}\".format(cm[i, j]),\n                     horizontalalignment=\"center\",\n                     color=\"white\" if cm[i, j] > thresh else \"black\")\n   ","metadata":{"execution":{"iopub.status.busy":"2022-01-03T06:03:58.059929Z","iopub.execute_input":"2022-01-03T06:03:58.060876Z","iopub.status.idle":"2022-01-03T06:03:58.070671Z","shell.execute_reply.started":"2022-01-03T06:03:58.060838Z","shell.execute_reply":"2022-01-03T06:03:58.069872Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"if TRAIN_MODEL: \n    model.load_weights(weight_path_save) # load the best model \nelse: \n    model.load_weights('../input/sartorius-best-model/best_model.hdf5') # load a previously trained model\n    model.summary()","metadata":{"execution":{"iopub.status.busy":"2022-01-03T06:03:58.071544Z","iopub.execute_input":"2022-01-03T06:03:58.071816Z","iopub.status.idle":"2022-01-03T06:03:59.122585Z","shell.execute_reply.started":"2022-01-03T06:03:58.071782Z","shell.execute_reply":"2022-01-03T06:03:59.120962Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"pred_prob = model.predict(valid_ds_batch, workers=4, verbose = True)\npred_labels = np.argmax(pred_prob, axis=-1)\n\nvalid_labels = np.concatenate([y.numpy() for x, y in valid_ds_batch], axis=0)\nprint('\\n', classification_report(valid_labels, pred_labels ))","metadata":{"execution":{"iopub.status.busy":"2022-01-03T06:03:59.123902Z","iopub.execute_input":"2022-01-03T06:03:59.124162Z","iopub.status.idle":"2022-01-03T06:07:46.692114Z","shell.execute_reply.started":"2022-01-03T06:03:59.124124Z","shell.execute_reply":"2022-01-03T06:07:46.691364Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"cm = confusion_matrix(valid_labels, pred_labels)\nclasses = CLASSES\ntitle = 'Confusion matrix of results'\nplot_confusion_matrix(cm, classes, title);","metadata":{"execution":{"iopub.status.busy":"2022-01-03T06:07:46.696923Z","iopub.execute_input":"2022-01-03T06:07:46.697145Z","iopub.status.idle":"2022-01-03T06:07:46.977461Z","shell.execute_reply.started":"2022-01-03T06:07:46.697111Z","shell.execute_reply":"2022-01-03T06:07:46.976743Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<a id='PartIII'></a>","metadata":{}},{"cell_type":"markdown","source":"### LIME (Local Interpretable Model-Agnostic Explanations)\n\nThere is a trade-off between model interpretability and accuracy: simple models are generally more interpretable while complex models have lower interpretability but higher accuracy. Deep learning models are black box models so in this trade-off they would also be categorized as having lower interpretability/higher accuracy.\n\nTo make deep learning models more explainable we can apply post hoc methods like LIME. The intuition behind LIME is to perturb the local area around a single data instance and then apply a simple, intrinsically interpretable, surrogate linear model to this neighborhood. This helps us interpret the outcome for an individual data point, but not the global model. LIME can be used for text, tabular or image data.\nFor image data specifically, the procedure can  be outlined  as follows:\n\n- generate multiple images from a given image by perturbing the image\n  (perturbing: randomly selecting superpixels from the given image)\n- use the trained model to predict the class for each of the generated images\n- weight each image perturbation as a function of its proximity to the original image\n- fit a weighted linear model (*explainer*) with the perturbed images as input and prediction as response\n- each superpixel in the image is ranked based on how much it contributes to the class\n\nIn this exercise, we will look at three images, one from each class. Since the classifer we trained was 100% accurate, all three images are true positive.","metadata":{}},{"cell_type":"markdown","source":"#### Select three images","metadata":{}},{"cell_type":"code","source":"# select a batch of images \nimage_iter_2, label_iter_2 = next(islice(valid_ds_batch, 2, None)) # access the 2nd batch from the iterator\n\nimage_iter_2 = tf.expand_dims(image_iter_2, 0)\nlabel_iter_2 = label_iter_2.numpy()\n\n# print labels for the batch of 8 images \nprint('The labels for the selected batch of 8 images are: ', label_iter_2)","metadata":{"execution":{"iopub.status.busy":"2022-01-03T06:07:46.978809Z","iopub.execute_input":"2022-01-03T06:07:46.979284Z","iopub.status.idle":"2022-01-03T06:07:47.156728Z","shell.execute_reply.started":"2022-01-03T06:07:46.979217Z","shell.execute_reply":"2022-01-03T06:07:47.155964Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# select an image for each unique label\nimage_idx_selected = [1,4,0]\n\nprint(f'The selected images are labeled: ')\nfor i in image_idx_selected:\n    print(f'{rev_label_dict.get(label_iter_2[i])}')","metadata":{"execution":{"iopub.status.busy":"2022-01-03T06:07:47.158257Z","iopub.execute_input":"2022-01-03T06:07:47.158761Z","iopub.status.idle":"2022-01-03T06:07:47.166245Z","shell.execute_reply.started":"2022-01-03T06:07:47.158719Z","shell.execute_reply":"2022-01-03T06:07:47.16556Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def explain_image_by_lime(image, label):\n    explanation = explainer.explain_instance(image, model.predict, \n                                             top_labels=3, hide_color=0, num_samples=1000)\n    \n    # show the top 3 superpixels that contribute to the class; do not show the rest of the image\n    temp_1, mask_1 = explanation.get_image_and_mask(explanation.top_labels[0], \n                                                positive_only=True, num_features=3, hide_rest=True)\n\n    # show the top 3 positive or negative superpixels; make the entire image visible\n    temp_2, mask_2 = explanation.get_image_and_mask(explanation.top_labels[0], \n                                                positive_only=False, num_features=3, hide_rest=False)\n    \n  \n\n    fig = plt.figure(figsize=(16, 12), constrained_layout=True)    \n    plt.subplot(1, 3, 1)\n    plt.imshow(image, cmap='gray')\n    plt.title('original image', fontsize=14)\n    plt.axis('off')\n    plt.subplot(1, 3, 2)\n    plt.imshow(mark_boundaries(temp_1, mask_1))\n    plt.title('positive superpixels', fontsize=14)\n    plt.axis('off')\n    plt.subplot(1, 3, 3)\n    plt.imshow(mark_boundaries(temp_2, mask_2), cmap='gray')\n    plt.title('positive & negative superpixels', fontsize=14)\n    plt.axis('off')\n    plt.suptitle(label, y=0.8, fontsize=18)\n    plt.show()","metadata":{"execution":{"iopub.status.busy":"2022-01-03T06:07:47.167786Z","iopub.execute_input":"2022-01-03T06:07:47.168317Z","iopub.status.idle":"2022-01-03T06:07:47.178925Z","shell.execute_reply.started":"2022-01-03T06:07:47.168279Z","shell.execute_reply":"2022-01-03T06:07:47.178082Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### Image 1","metadata":{}},{"cell_type":"code","source":"image = image_iter_2[0][1].numpy().astype('double')\nlabel = rev_label_dict.get(label_iter_2[1])\nprint(f'The image selected is labeled: {label}\\n')\n\nexplain_image_by_lime(image, label)","metadata":{"execution":{"iopub.status.busy":"2022-01-03T06:07:47.180295Z","iopub.execute_input":"2022-01-03T06:07:47.180817Z","iopub.status.idle":"2022-01-03T06:07:57.354745Z","shell.execute_reply.started":"2022-01-03T06:07:47.180779Z","shell.execute_reply":"2022-01-03T06:07:57.354073Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"On the left is the original *shsy5y* image. The image is slightly blurred near the edges.  \n\nThe image in the center *explains* the prediction. It shows the superpixels that most positively contribute to the correct classification of the image. These superpixels which are from the least blurred parts of the image, show the evenly distributed *shsy5y* cells.   \n\nOn the right the green superpixel is used to positively identify the class. The red negatively contributes to the class. We can see that the regions corresponding to the red in the original image are blurred edges.","metadata":{}},{"cell_type":"markdown","source":"#### Image 2","metadata":{}},{"cell_type":"code","source":"image = image_iter_2[0][4].numpy().astype('double')\nlabel = rev_label_dict.get(label_iter_2[4])\nprint(f'The image selected is labeled: {label}\\n')\n\nexplain_image_by_lime(image, label)","metadata":{"execution":{"iopub.status.busy":"2022-01-03T06:10:01.498764Z","iopub.execute_input":"2022-01-03T06:10:01.499022Z","iopub.status.idle":"2022-01-03T06:10:10.169722Z","shell.execute_reply.started":"2022-01-03T06:10:01.498994Z","shell.execute_reply":"2022-01-03T06:10:10.169043Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"On the left we see the original *astro* image. The lower left corner is blurred/smudged and appears empty.   \n\nIn the center, we see the superpixels that most positively contribute to the *astro* classification. One superpixel captures the large blank spaces that surround the elongated cell projections. This is the characterizing visual  feature that I would also use to classify an image as *astro*. Note the superpixel at the lower left corner; that is the section where the image is smudged and is probably being treated as a blank space.\n\nOn the right we see the superpixels which contribute positively (green) to the identification and negatively (red). The red superpixel includes a region dense with cells that do not appear to have the characteristic elongated radiations.","metadata":{}},{"cell_type":"markdown","source":"#### Image 3","metadata":{}},{"cell_type":"code","source":"image = image_iter_2[0][0].numpy().astype('double')\nlabel = rev_label_dict.get(label_iter_2[0])\nprint(f'The image selected is labeled: {label}\\n')\n\nexplain_image_by_lime(image, label)","metadata":{"execution":{"iopub.status.busy":"2022-01-03T06:33:44.702101Z","iopub.execute_input":"2022-01-03T06:33:44.702887Z","iopub.status.idle":"2022-01-03T06:33:53.70973Z","shell.execute_reply.started":"2022-01-03T06:33:44.702846Z","shell.execute_reply":"2022-01-03T06:33:53.709081Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"In the original image on the left, we see larger *cort* cell clusters with smaller cells/artifacts scattered in the background.\n\nIn the center image, the two superpixels that most strongly contribute to the positive identification highlight the background cells/artifacts we see in *cort* images but not *shsy5y* images.\n\nOn the right, the green superpixel positively identifies the image; the red negatively identifies the class. It is possible that the red superpixel, which juxtaposes a cell cluster, is being identified as the negative space in an *astro* cell.","metadata":{}},{"cell_type":"markdown","source":"## Summary\n\nThe deep learning model appears to have used some of the same visual cues I used to classify the images. To wit: \n- the *shsy5y* cells were identified by the size and distribution of the cells.  \n- the *astro* cells were identified by the sparse negative space between cells.  \n- the *cort* cells are also identified by the background.  \n\nThe reader is invited to explore and interpret LIME explanations with a different set of images.","metadata":{}},{"cell_type":"markdown","source":"## References\n\n<a name='ref1'></a>[1] [M. Ribeiro, S. Singh, C. Guestrin,\n'\"Why Should I Trust You?\": Explaining the Predictions of Any Classifier.' arXiv:1602.04938](https://arxiv.org/abs/1602.04938)","metadata":{}},{"cell_type":"markdown","source":"**Author: Meena Mani**        \n**Date: January 2022**","metadata":{}}]}