{"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.initializers import Constant\nfrom keras.models import Sequential\nfrom keras.optimizers import Adam\nfrom tensorflow.python.ops import array_ops\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), 4000)\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":"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        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        directory='/kaggle/tmp/',\n        x_col='filename', \n        y_col=['any', 'epidural', 'intraparenchymal', \n               'intraventricular', 'subarachnoid', 'subdural'],\n        class_mode='multi_output',\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 focal_loss(prediction_tensor, target_tensor, weights=None, alpha=0.25, gamma=2):\n    r\"\"\"Compute focal loss for predictions.\n        Multi-labels Focal loss formula:\n            FL = -alpha * (z-p)^gamma * log(p) -(1-alpha) * p^gamma * log(1-p)\n                 ,which alpha = 0.25, gamma = 2, p = sigmoid(x), z = target_tensor.\n    Args:\n     prediction_tensor: A float tensor of shape [batch_size, num_anchors,\n        num_classes] representing the predicted logits for each class\n     target_tensor: A float tensor of shape [batch_size, num_anchors,\n        num_classes] representing one-hot encoded classification targets\n     weights: A float tensor of shape [batch_size, num_anchors]\n     alpha: A scalar tensor for focal loss alpha hyper-parameter\n     gamma: A scalar tensor for focal loss gamma hyper-parameter\n    Returns:\n        loss: A (scalar) tensor representing the value of the loss function\n    \"\"\"\n    sigmoid_p = tf.nn.sigmoid(prediction_tensor)\n    zeros = array_ops.zeros_like(sigmoid_p, dtype=sigmoid_p.dtype)\n    \n    # For poitive prediction, only need consider front part loss, back part is 0;\n    # target_tensor > zeros <=> z=1, so poitive coefficient = z - p.\n    pos_p_sub = array_ops.where(target_tensor > zeros, target_tensor - sigmoid_p, zeros)\n    \n    # For negative prediction, only need consider back part loss, front part is 0;\n    # target_tensor > zeros <=> z=1, so negative coefficient = 0.\n    neg_p_sub = array_ops.where(target_tensor > zeros, zeros, sigmoid_p)\n    per_entry_cross_ent = - alpha * (pos_p_sub ** gamma) * tf.log(tf.clip_by_value(sigmoid_p, 1e-8, 1.0)) \\\n                          - (1 - alpha) * (neg_p_sub ** gamma) * tf.log(tf.clip_by_value(1.0 - sigmoid_p, 1e-8, 1.0))\n    return tf.reduce_sum(per_entry_cross_ent)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"densenet = DenseNet121(\n    weights='../input/densenet-keras/DenseNet-BC-121-32-no-top.h5',\n    include_top=False,\n    input_shape=(224,224,3)\n)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def build_model():\n    model = Sequential()\n    model.add(densenet)\n    model.add(layers.GlobalAveragePooling2D())\n    model.add(layers.Dropout(0.5))\n#     model.add(layers.Dense(6, activation='sigmoid', \n#                            bias_initializer=Constant(value=-5.5)))\n    model.add(layers.Dense(6, activation='sigmoid'))\n    \n    model.compile(\n#         loss=focal_loss,\n        loss='categorical_crossentropy',\n        optimizer=Adam(lr=0.001),\n        metrics=['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":"checkpoint = ModelCheckpoint(\n    'model.h5', \n    monitor='val_loss', \n    verbose=0, \n    save_best_only=True, \n    save_weights_only=False,\n    mode='auto'\n)\n\ntotal_steps = sample_files.shape[0] / BATCH_SIZE\n\nhistory = 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=[checkpoint],\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}