{"cells":[{"metadata":{},"cell_type":"markdown","source":"# About this kernel\n\nIn this kernel, I go through each steps of the process for building a multi-label classifier in the RSNA Intracranial Hemorrhage competition. I will be only using a subset of the total dataset.\n\n## Notes\n\n* V7: I noticed that it takes roughly 1000 seconds to run 2000 iterations. Therefore, I set each batch to be 2000 iterations, so that we can estimate the total running time to be a multiple of 1000 seconds (and at every slice of 2000 iterations, we can get the validation results)."},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"_kg_hide-output":true},"cell_type":"code","source":"import os\nimport json\n\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport pydicom\nfrom keras import layers\nfrom keras.applications import DenseNet121\nfrom keras.preprocessing.image import ImageDataGenerator\nfrom keras.callbacks import Callback, ModelCheckpoint\nfrom keras.preprocessing.image import ImageDataGenerator\nfrom keras.models import Sequential\nfrom keras.optimizers import Adam\nfrom tqdm import tqdm\n\n  \nfrom keras import backend as K\nimport tensorflow as tf","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","trusted":true},"cell_type":"code","source":"BASE_PATH = '/kaggle/input/rsna-intracranial-hemorrhage-detection/'\nTRAIN_DIR = 'stage_1_train_images/'\nTEST_DIR = 'stage_1_test_images/'","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Load CSVs"},{"metadata":{"trusted":true},"cell_type":"code","source":"train_df = pd.read_csv(BASE_PATH + 'stage_1_train.csv')\nsub_df = pd.read_csv(BASE_PATH + 'stage_1_sample_submission.csv')\n\ntrain_df['filename'] = train_df['ID'].apply(lambda st: \"ID_\" + st.split('_')[1] + \".png\")\ntrain_df['type'] = train_df['ID'].apply(lambda st: st.split('_')[2])\nsub_df['filename'] = sub_df['ID'].apply(lambda st: \"ID_\" + st.split('_')[1] + \".png\")\nsub_df['type'] = sub_df['ID'].apply(lambda st: st.split('_')[2])\n\nprint(train_df.shape)\ntrain_df.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"test_df = pd.DataFrame(sub_df.filename.unique(), columns=['filename'])\nprint(test_df.shape)\ntest_df.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"We only select a sample of the total training dataset (we randomly choose 400k files), and call it `sample_df`."},{"metadata":{"trusted":true},"cell_type":"code","source":"np.random.seed(1749)\nsample_files = np.random.choice(os.listdir(BASE_PATH + TRAIN_DIR), 400000)\nsample_df = train_df[train_df.filename.apply(lambda x: x.replace('.png', '.dcm')).isin(sample_files)]","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"`pivot_df` is simply `sample_df` reformatted so that each column is a label (this way, we can use multi-label in our data generator later)."},{"metadata":{"trusted":true},"cell_type":"code","source":"pivot_df = sample_df[['Label', 'filename', 'type']].drop_duplicates().pivot(\n    index='filename', columns='type', values='Label').reset_index()\nprint(pivot_df.shape)\npivot_df.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Rescale, Resize and Convert to PNG"},{"metadata":{},"cell_type":"markdown","source":"Source: https://www.kaggle.com/omission/eda-view-dicom-images-with-correct-windowing"},{"metadata":{"trusted":true},"cell_type":"code","source":"def window_image(img, window_center,window_width, intercept, slope, rescale=True):\n\n    img = (img*slope +intercept)\n    img_min = window_center - window_width//2\n    img_max = window_center + window_width//2\n    img[img<img_min] = img_min\n    img[img>img_max] = img_max\n    \n    if rescale:\n        # Extra rescaling to 0-1, not in the original notebook\n        img = (img - img_min) / (img_max - img_min)\n    \n    return img\n    \ndef get_first_of_dicom_field_as_int(x):\n    #get x[0] as in int is x is a 'pydicom.multival.MultiValue', otherwise get int(x)\n    if type(x) == pydicom.multival.MultiValue:\n        return int(x[0])\n    else:\n        return int(x)\n\ndef get_windowing(data):\n    dicom_fields = [data[('0028','1050')].value, #window center\n                    data[('0028','1051')].value, #window width\n                    data[('0028','1052')].value, #intercept\n                    data[('0028','1053')].value] #slope\n    return [get_first_of_dicom_field_as_int(x) for x in dicom_fields]","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def save_and_resize(filenames, load_dir):    \n    save_dir = '/kaggle/tmp/'\n    if not os.path.exists(save_dir):\n        os.makedirs(save_dir)\n\n    for filename in tqdm(filenames):\n        path = load_dir + filename\n        new_path = save_dir + filename.replace('.dcm', '.png')\n        \n        dcm = pydicom.dcmread(path)\n        window_center , window_width, intercept, slope = get_windowing(dcm)\n        img = dcm.pixel_array\n        img = window_image(img, window_center, window_width, intercept, slope)\n        \n        resized = cv2.resize(img, (224, 224))\n        res = cv2.imwrite(new_path, resized)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"save_and_resize(filenames=sample_files, load_dir=BASE_PATH + TRAIN_DIR)\nsave_and_resize(filenames=os.listdir(BASE_PATH + TEST_DIR), load_dir=BASE_PATH + TEST_DIR)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Data Generator"},{"metadata":{"trusted":true},"cell_type":"code","source":"pivot_df","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"test_df","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"BATCH_SIZE = 64\n\ndef create_datagen():\n    return ImageDataGenerator(validation_split=0.15)\n\ndef create_test_gen():\n    return ImageDataGenerator().flow_from_dataframe(\n        test_df,\n        color_mode=\"rgb\",\n        directory='/kaggle/tmp/',\n        x_col='filename',\n        class_mode=None,\n        target_size=(224, 224),\n        batch_size=BATCH_SIZE,\n        shuffle=False\n    )\n\ndef create_flow(datagen, subset):\n    return datagen.flow_from_dataframe(\n        pivot_df, \n        color_mode=\"rgb\",\n        directory='/kaggle/tmp/',\n        x_col='filename',\n        y_col=['any', 'epidural', 'intraparenchymal', \n               'intraventricular', 'subarachnoid', 'subdural'],\n        class_mode='raw',\n        target_size=(224, 224),\n        batch_size=BATCH_SIZE,\n        subset=subset\n    )\n\n# Using original generator\ndata_generator = create_datagen()\ntrain_gen = create_flow(data_generator, 'training')\nval_gen = create_flow(data_generator, 'validation')\ntest_gen = create_test_gen()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Train Model"},{"metadata":{},"cell_type":"markdown","source":"### Loss function\n\nFocal loss is good for unbalanced datasets, like this one."},{"metadata":{"trusted":true},"cell_type":"code","source":"def binary_focal_loss(gamma=2., alpha=.25):\n    \"\"\"\n    Binary form of focal loss.\n      FL(p_t) = -alpha * (1 - p_t)**gamma * log(p_t)\n      where p = sigmoid(x), p_t = p or 1 - p depending on if the label is 1 or 0, respectively.\n    References:\n        https://arxiv.org/pdf/1708.02002.pdf\n    Usage:\n     model.compile(loss=[binary_focal_loss(alpha=.25, gamma=2)], metrics=[\"accuracy\"], optimizer=adam)\n    \"\"\"\n    def binary_focal_loss_fixed(y_true, y_pred):\n        \"\"\"\n        :param y_true: A tensor of the same shape as `y_pred`\n        :param y_pred:  A tensor resulting from a sigmoid\n        :return: Output tensor.\n        \"\"\"\n        pt_1 = tf.where(tf.equal(y_true, 1), y_pred, tf.ones_like(y_pred))\n        pt_0 = tf.where(tf.equal(y_true, 0), y_pred, tf.zeros_like(y_pred))\n\n        epsilon = K.epsilon()\n        # clip to prevent NaN's and Inf's\n        pt_1 = K.clip(pt_1, epsilon, 1. - epsilon)\n        pt_0 = K.clip(pt_0, epsilon, 1. - epsilon)\n\n        return -K.sum(alpha * K.pow(1. - pt_1, gamma) * K.log(pt_1)) \\\n               -K.sum((1 - alpha) * K.pow(pt_0, gamma) * K.log(1. - pt_0))\n\n    return binary_focal_loss_fixed","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"import keras\nmobile_net = keras.applications.mobilenet_v2.MobileNetV2(\n    include_top=False, weights='imagenet', input_shape=(224, 224, 3), pooling='avg')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def build_model():\n    model = Sequential()\n    model.add(mobile_net)\n    model.add(layers.Dense(6, activation='sigmoid'))\n    \n    model.compile(\n        loss=binary_focal_loss(),\n        optimizer=Adam(lr=1e-4),\n        metrics=['binary_accuracy']\n    )\n    \n    return model","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"model = build_model()\nmodel.summary()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"model.input_shape","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"from keras.callbacks import *\n\ncheckpoint = ModelCheckpoint(\n    'model.h5', \n    monitor='val_loss', \n    verbose=0, \n    save_best_only=True, \n    save_weights_only=False,\n    mode='min'\n)\nreduceLROnPlat = ReduceLROnPlateau(\n    monitor='val_loss', \n    factor=0.1, \n    patience=5, \n    verbose=1, \n    mode='min',\n    cooldown=5, \n    min_lr=1e-6,\n)\nearly = EarlyStopping(\n    monitor=\"val_loss\",\n    mode=\"min\",\n    patience=25,\n)\n\ncallbacks_list = [checkpoint, early, reduceLROnPlat]\n\ntotal_steps = sample_files.shape[0] / BATCH_SIZE","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"history = model.fit_generator(\n    train_gen,\n    steps_per_epoch=2000,\n    validation_data=val_gen,\n    validation_steps=total_steps * 0.15,\n    callbacks=callbacks_list,\n    epochs=5\n)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"with open('history.json', 'w') as f:\n    json.dump(history.history, f)\n\nhistory_df = pd.DataFrame(history.history)\nhistory_df[['loss', 'val_loss']].plot()\nhistory_df[['acc', 'val_acc']].plot()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Submission"},{"metadata":{"trusted":true},"cell_type":"code","source":"model.load_weights('model.h5')\ny_test = model.predict_generator(\n    test_gen,\n    steps=len(test_gen),\n    verbose=1\n)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Append the output predicts in the wide format to the y_test\ntest_df = test_df.join(pd.DataFrame(y_test, columns=[\n    'any', 'epidural', 'intraparenchymal', 'intraventricular', 'subarachnoid', 'subdural'\n]))\n\n# Unpivot table, i.e. wide (N x 6) to long format (6N x 1)\ntest_df = test_df.melt(id_vars=['filename'])\n\n# Combine the filename column with the variable column\ntest_df['ID'] = test_df.filename.apply(lambda x: x.replace('.png', '')) + '_' + test_df.variable\ntest_df['Label'] = test_df['value']\n\ntest_df[['ID', 'Label']].to_csv('submission.csv', index=False)","execution_count":null,"outputs":[]}],"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":4,"nbformat_minor":1}