{"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":"# RSNA-MICCAI Brain Tumor Radiogenomic Classification","metadata":{}},{"cell_type":"markdown","source":"・ このノートブックでは、TensorFlowの学習のために、VGG16モデルを用いた予測モデルの構築を行いました。\n\n・ TensorFlowのコーディングには、公式サイトや下記日本語サイトなどにお世話になりました。\n\n　　[@IT; TensorFlow 2＋Keras（tf.keras）入門][1]\n\n　　[TensorFlowの公式チュートリアル「tf.dataを使って画像をロードする」をもうちょっとスリムにして読む][2]\n\n\n・ 今回の予測精度はサッパリですが、今後は用いるデータを変えたり、3DCNNモデルを用いるなどして改善できればと思っています。\n\n・ 初学者なので、よからぬ書き方や間違っている箇所などもあるかと思います。お気づきの際には教えていただけるとありがたいです。\n\n・ その他、ご意見ご感想などもいただけると嬉しく思います。よろしくお願いします。\n\n[* The English version of this notebook][5]\n\n---\n【入力データについて】\n\n・ VGG16の入力チャネルは3なので、\"FLAIR\", \"T1w\", \"T2w\"の画像3枚をconcatして1つのデータにしています。\n\n・ そのために、各BraTS21IDの\"FLAIR\", \"T1w\", \"T2w\"フォルダから、中央の画像データを取得しています。\n\n---\nEDAに関して、下記ノートブックを参考にさせていただきました。ありがとうございます。\n\n1. [[TF]: 3D & 2D Model for Brain Tumor Classification][3]\n\n2. [【Brain Tumor】EDA for starter(日本語version)][4]\n\n---\n\n[1]: https://atmarkit.itmedia.co.jp/ait/subtop/features/di/tf2keras_index.html\n \n[2]: https://zenn.dev/tokyoyoshida/articles/5c3270ce0d4c91\n\n[3]: https://www.kaggle.com/ipythonx/tf-3d-2d-model-for-brain-tumor-classification\n\n[4]: https://www.kaggle.com/chumajin/brain-tumor-eda-for-starter-version\n\n[5]: https://www.kaggle.com/masatomurakawamm/tensorflow-simple-prediction-with-2d-vgg16","metadata":{}},{"cell_type":"markdown","source":"# 0. Settings","metadata":{}},{"cell_type":"code","source":"# Import dependencies \nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt \n%matplotlib inline\n\nimport os, sys, glob, gc \nimport math, random, time\nfrom tqdm import tqdm \nimport cv2, pydicom\n\nfrom sklearn.model_selection import StratifiedKFold \n\nimport tensorflow as tf","metadata":{"execution":{"iopub.status.busy":"2021-08-29T03:57:32.945234Z","iopub.execute_input":"2021-08-29T03:57:32.94715Z","iopub.status.idle":"2021-08-29T03:57:41.15028Z","shell.execute_reply.started":"2021-08-29T03:57:32.94659Z","shell.execute_reply":"2021-08-29T03:57:41.149165Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Params\nconfig = {\n    'data_path': '../input/rsna-miccai-brain-tumor-radiogenomic-classification',\n    'model_path': '../input/keras-pretrained-models/vgg16_weights_tf_dim_ordering_tf_kernels_notop.h5',\n    'input_path': '../input', \n    'output_path': './',\n    'nfolds': 5, \n    'batch_size': 16,\n    'learning_rate': 1e-4,\n    'num_epochs': 10\n}\nAUTO = tf.data.AUTOTUNE\n\n# For reproducible results    \ndef seed_all(s):\n    random.seed(s)\n    np.random.seed(s)\n    tf.random.set_seed(s)\n    os.environ['TF_CUDNN_DETERMINISTIC'] = '1'\n    os.environ['PYTHONHASHSEED'] = str(s) \nglobal_seed = 42\nseed_all(global_seed)\n\ninput_modality = [\"FLAIR\", \"T1w\", \"T1wCE\", \"T2w\"]\nmodality_list = [\"FLAIR\", \"T1w\", \"T2w\"] \n\ntrain_folder = os.path.join(config['data_path'], 'train')\ntest_folder = os.path.join(config['data_path'], 'test')\nsample_submission_path = os.path.join(config['data_path'], 'sample_submission.csv')\n\ntrain_df = pd.read_csv(os.path.join(config['data_path'], 'train_labels.csv')); print(train_df.shape)\nsample_df = pd.read_csv(sample_submission_path); print(sample_df.shape)\ntest_df = sample_df.copy(); print(test_df.shape)","metadata":{"execution":{"iopub.status.busy":"2021-08-29T03:57:45.438221Z","iopub.execute_input":"2021-08-29T03:57:45.438566Z","iopub.status.idle":"2021-08-29T03:57:45.470148Z","shell.execute_reply.started":"2021-08-29T03:57:45.438536Z","shell.execute_reply":"2021-08-29T03:57:45.469036Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 1. EDA","metadata":{}},{"cell_type":"markdown","source":"## 1.1 Train DataFrame","metadata":{}},{"cell_type":"code","source":"# 交差検証用に5分割（しかし、このノートブックではホールドアウト検証しかしていない）\n\nskf = StratifiedKFold(n_splits=config['nfolds'], shuffle=True, random_state=global_seed)\n\nfor index, (train_index, val_index) in enumerate(skf.split(X=train_df.index, y=train_df.MGMT_value)):\n    train_df.loc[val_index, 'fold'] = index\n    \nprint(train_df.groupby(['fold', train_df.MGMT_value]).size())","metadata":{"execution":{"iopub.status.busy":"2021-08-29T03:57:48.624465Z","iopub.execute_input":"2021-08-29T03:57:48.62499Z","iopub.status.idle":"2021-08-29T03:57:48.659686Z","shell.execute_reply.started":"2021-08-29T03:57:48.624945Z","shell.execute_reply":"2021-08-29T03:57:48.658908Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# BraTS21IDごとにフォルダパスを取得\n\ntrain_df['imfolder'] = ['{:05d}'.format(s) for s in train_df['BraTS21ID']]\ntrain_df['path'] = [os.path.join(train_folder, s) for s in train_df['imfolder']]\ntrain_df","metadata":{"execution":{"iopub.status.busy":"2021-08-29T03:57:55.962536Z","iopub.execute_input":"2021-08-29T03:57:55.963066Z","iopub.status.idle":"2021-08-29T03:57:55.996042Z","shell.execute_reply.started":"2021-08-29T03:57:55.963019Z","shell.execute_reply":"2021-08-29T03:57:55.995066Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# BraTS21IDごとに、\"FLAIR\", \"T1w\", \"T1wCE\", \"T2w\"の各フォルダの中身をカウント\n\ninput_modality = [\"FLAIR\", \"T1w\", \"T1wCE\", \"T2w\"] \nfor modality in input_modality:   \n    modality_count = []\n    for i in range(len(train_df)):\n        sample_folder = train_df['path'].iloc[i]\n        modality_folder = os.path.join(sample_folder, modality)\n        if os.path.exists(modality_folder):\n            modality_count.append(len(os.listdir(modality_folder)))\n        else:\n            modality_count.append(0)\n        \n    train_df[f'{modality}_count'] = modality_count    \n    \ntrain_df","metadata":{"execution":{"iopub.status.busy":"2021-08-29T04:02:07.828334Z","iopub.execute_input":"2021-08-29T04:02:07.828726Z","iopub.status.idle":"2021-08-29T04:03:15.054433Z","shell.execute_reply.started":"2021-08-29T04:02:07.828693Z","shell.execute_reply":"2021-08-29T04:03:15.053481Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# 先のカウント数をもとに、\"FLAIR\", \"T1w\", \"T2w\"フォルダごとに中央の画像データパスを取得\n\ndef get_mid_path(path, modality='FLAIR'):\n    modality_path = os.path.join(path, modality)\n    img_list = os.listdir(modality_path)\n\n    img_num = [s.split('-')[1] for s in img_list]\n    img_num = [s.split('.')[0] for s in img_num]\n\n    img_path_list = [os.path.join(modality_path, s) for s in img_list]\n\n    tempdf = pd.DataFrame()\n    tempdf['img_num'] = img_num\n    tempdf['img_num'] = tempdf['img_num'].astype('int')\n    tempdf['img_path'] = img_path_list\n\n    tempdf = tempdf.sort_values('img_num').reset_index(drop=True)\n\n    num_imgs = len(img_list)\n    mid_img_path = tempdf['img_path'].iloc[num_imgs//2]\n    return mid_img_path\n\nfor modality in input_modality:\n    train_df[f'{modality}_mid'] = [ get_mid_path(s, modality=modality) for s in train_df['path'] ]\n    \ntrain_df","metadata":{"execution":{"iopub.status.busy":"2021-08-29T04:06:36.94752Z","iopub.execute_input":"2021-08-29T04:06:36.94792Z","iopub.status.idle":"2021-08-29T04:06:46.783296Z","shell.execute_reply.started":"2021-08-29T04:06:36.947886Z","shell.execute_reply":"2021-08-29T04:06:46.782277Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## 1.2 Test DataFrame（先のTrain DataFrameと同様の流れ）","metadata":{}},{"cell_type":"code","source":"test_df['imfolder'] = ['{:05d}'.format(s) for s in test_df['BraTS21ID']]\ntest_df['path'] = [os.path.join(test_folder, s) for s in test_df['imfolder']]\ntest_df","metadata":{"execution":{"iopub.status.busy":"2021-08-29T04:06:51.920663Z","iopub.execute_input":"2021-08-29T04:06:51.921073Z","iopub.status.idle":"2021-08-29T04:06:51.942299Z","shell.execute_reply.started":"2021-08-29T04:06:51.921034Z","shell.execute_reply":"2021-08-29T04:06:51.941194Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"input_modality = [\"FLAIR\", \"T1w\", \"T1wCE\", \"T2w\"] \nfor modality in input_modality:   \n    modality_count = []\n    for i in range(len(test_df)):\n        sample_folder = test_df['path'].iloc[i]\n        modality_folder = os.path.join(sample_folder, modality)\n        if os.path.exists(modality_folder):\n            modality_count.append(len(os.listdir(modality_folder)))\n        else:\n            modality_count.append(0)\n        \n    test_df[f'{modality}_count'] = modality_count    \n    \ntest_df","metadata":{"execution":{"iopub.status.busy":"2021-08-29T04:06:57.2793Z","iopub.execute_input":"2021-08-29T04:06:57.279665Z","iopub.status.idle":"2021-08-29T04:07:03.311468Z","shell.execute_reply.started":"2021-08-29T04:06:57.279635Z","shell.execute_reply":"2021-08-29T04:07:03.310414Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"for modality in input_modality:\n    test_df[f'{modality}_mid'] = [ get_mid_path(s, modality=modality) for s in test_df['path'] ]\n    \ntest_df","metadata":{"execution":{"iopub.status.busy":"2021-08-29T04:07:05.920592Z","iopub.execute_input":"2021-08-29T04:07:05.921002Z","iopub.status.idle":"2021-08-29T04:07:07.289888Z","shell.execute_reply.started":"2021-08-29T04:07:05.920946Z","shell.execute_reply":"2021-08-29T04:07:07.288924Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 2. DataLoader","metadata":{}},{"cell_type":"markdown","source":"## 2.1 Train Dataset","metadata":{}},{"cell_type":"markdown","source":"・ はじめは単純にtf.data.Dataset.from_tensor_slices()を使おうと思っていたのですが、dcmファイルの読み込みが上手くいかず、Keras Sequenceを使いました。\n\n・ 高速化のため、TFRecordの作成と読み込みも行なっています。","metadata":{}},{"cell_type":"code","source":"@tf.function\ndef preprocessing_img(img, threashold=5):\n    img_mean = tf.math.reduce_mean(img)\n    img = img - img_mean\n    img_var = tf.math.reduce_variance(img)\n    img = img / img_var\n    img_min = tf.math.reduce_min(img)\n    img = img - img_min\n    img = tf.where(img<threashold, img, threashold)    # 入力値が大きくなりすぎる場合には制限\n    img = tf.squeeze(img)    # 余計な次元はカット\n    return img\n\n    \nclass ImageGenerator(tf.keras.utils.Sequence):\n    def __init__(self, df, modality_list):\n        self.df = df\n        self.modality_list = modality_list\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, index):\n        img_list = []\n        for modality in self.modality_list:\n            path = self.df[f'{modality}_mid'].iloc[index]\n            dicom = pydicom.read_file(path)\n            img = dicom.pixel_array\n            img = np.expand_dims(img, -1)\n            img = tf.constant(img)\n            img = tf.image.resize(img, [224, 224])    # concatするため、ここで画像の大きさを揃える\n            img_list.append(img)\n        multi_ch_img = tf.concat(img_list, axis=-1)\n        multi_ch_img = preprocessing_img(multi_ch_img)\n        return multi_ch_img         # shape=(224, 224, 3)\n    \n    \ndef parse(x):\n    result = tf.io.parse_tensor(x, out_type=tf.float32)\n    result = tf.reshape(result, [224, 224, 3])\n    return result\n\n\ndef build_3ch_train_dataloader(train_df, modality_list, p_fold=0):\n    p_train = train_df.query(f'fold != {p_fold}').reset_index(drop=True)\n    p_valid = train_df.query(f'fold == {p_fold}').reset_index(drop=True)\n\n    AUTOTUNE = tf.data.experimental.AUTOTUNE\n\n    train_datasets = []\n    for mode, df in zip(['train', 'valid'], [p_train, p_valid]):\n        i_g = ImageGenerator(df, modality_list)\n        img_ds = tf.data.Dataset.from_generator(lambda: map(tuple, i_g),\n                                                output_types=(tf.float32),\n                                                output_shapes=(tf.TensorShape([224, 224, 3])),\n                                                 )\n        \n        serial_ds = img_ds.map(tf.io.serialize_tensor)\n\n        if not os.path.exists(f'{mode}-{p_fold}-img.tfrec'):\n            img_tfrec = tf.data.experimental.TFRecordWriter(f'{mode}-{p_fold}-img.tfrec')\n            img_tfrec.write(serial_ds)\n        serial_ds = tf.data.TFRecordDataset(f'{mode}-{p_fold}-img.tfrec')\n        serial_ds = serial_ds.map(parse, num_parallel_calls=AUTOTUNE)\n\n        labels = df['MGMT_value']\n        label_ds = tf.data.Dataset.from_tensor_slices(tf.cast(labels, tf.int32))\n\n        ds = tf.data.Dataset.zip((img_ds, label_ds))\n        \n        ds = ds.cache(filename=f'./cache.tf-{mode}-{p_fold}-data')\n        if mode == 'train':\n            train_count = len(df)\n            ds = ds.shuffle(buffer_size=train_count)\n        ds = ds.batch(config['batch_size'], drop_remainder=True)\n        ds = ds.prefetch(buffer_size=AUTOTUNE)\n        train_datasets.append(ds)\n\n    return train_datasets","metadata":{"execution":{"iopub.status.busy":"2021-08-29T04:19:48.199722Z","iopub.execute_input":"2021-08-29T04:19:48.200106Z","iopub.status.idle":"2021-08-29T04:19:48.222143Z","shell.execute_reply.started":"2021-08-29T04:19:48.20007Z","shell.execute_reply":"2021-08-29T04:19:48.221037Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Datasetの作成\np_fold = 0\nmodality_list = [\"FLAIR\", \"T1w\", \"T2w\"] \n\ntrain_datasets = build_3ch_train_dataloader(train_df, modality_list, p_fold=p_fold)\ntrain_ds = train_datasets[0]\nvalid_ds = train_datasets[1]\n\nfor d, l in train_ds.take(1):\n    print('Train Data shape: ', d.shape)\n    print('Train Label shape: ', l.shape)\n    \nfor d, l in valid_ds.take(1):\n    print('Valid Data shape: ', d.shape)\n    print('Valid Label shape: ', l.shape)","metadata":{"execution":{"iopub.status.busy":"2021-08-29T04:22:30.017743Z","iopub.execute_input":"2021-08-29T04:22:30.01816Z","iopub.status.idle":"2021-08-29T04:22:31.568137Z","shell.execute_reply.started":"2021-08-29T04:22:30.018125Z","shell.execute_reply":"2021-08-29T04:22:31.567093Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## 2.2 Test Dataset","metadata":{}},{"cell_type":"code","source":"# ラベルなしのTestDatasetを作成\ndef build_3ch_test_dataloader(test_df, modality_list):\n    AUTOTUNE = tf.data.experimental.AUTOTUNE\n\n    i_g = ImageGenerator(test_df, modality_list)\n    img_ds = tf.data.Dataset.from_generator(lambda: map(tuple, i_g),\n                                         output_types=(tf.float32),\n                                         output_shapes=(tf.TensorShape([224, 224, 3])),\n                                                 )\n    serial_ds = img_ds.map(tf.io.serialize_tensor)\n\n    if not os.path.exists('test-img.tfrec'):\n        img_tfrec = tf.data.experimental.TFRecordWriter('test-img.tfrec')\n        img_tfrec.write(serial_ds)\n    serial_ds = tf.data.TFRecordDataset('test-img.tfrec')\n    test_ds = serial_ds.map(parse, num_parallel_calls=AUTOTUNE)\n\n    test_ds = test_ds.cache(filename='./cache.tf-test-data')\n    test_ds = test_ds.batch(config['batch_size'], drop_remainder=False)\n    test_ds = test_ds.prefetch(buffer_size=AUTOTUNE)\n\n    return test_ds","metadata":{"execution":{"iopub.status.busy":"2021-08-29T04:23:20.382834Z","iopub.execute_input":"2021-08-29T04:23:20.383249Z","iopub.status.idle":"2021-08-29T04:23:20.393215Z","shell.execute_reply.started":"2021-08-29T04:23:20.383215Z","shell.execute_reply":"2021-08-29T04:23:20.392063Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"test_ds = build_3ch_test_dataloader(test_df, modality_list)\n\nfor d in test_ds.take(1):\n    print('Test Data shape: ', d.shape)","metadata":{"execution":{"iopub.status.busy":"2021-08-29T04:23:22.464739Z","iopub.execute_input":"2021-08-29T04:23:22.465151Z","iopub.status.idle":"2021-08-29T04:23:28.18237Z","shell.execute_reply.started":"2021-08-29T04:23:22.465119Z","shell.execute_reply":"2021-08-29T04:23:28.181121Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 3. Train","metadata":{}},{"cell_type":"code","source":"# モデル構築用関数\ndef build_model():    \n    vgg_layers = tf.keras.applications.vgg16.VGG16(weights=config['model_path'],\n                                          include_top=False,\n                                          pooling='avg',\n                                          input_shape=None)\n    vgg_layers.trainable = False\n    vgg_norm = tf.keras.layers.BatchNormalization(name='vgg_norm')\n    \n    model = tf.keras.Sequential()\n    model.add(vgg_layers)\n    model.add(vgg_norm)\n    model.add(tf.keras.layers.Dense(units=256, name='d_1', activation='relu'))\n    model.add(tf.keras.layers.Dense(units=1, name='d_out', activation='sigmoid'))\n    \n    return model","metadata":{"execution":{"iopub.status.busy":"2021-08-29T04:25:01.785144Z","iopub.execute_input":"2021-08-29T04:25:01.785757Z","iopub.status.idle":"2021-08-29T04:25:01.797338Z","shell.execute_reply.started":"2021-08-29T04:25:01.785708Z","shell.execute_reply":"2021-08-29T04:25:01.79607Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# モデル構築\nif tf.test.is_gpu_available():\n    device_name = tf.test.gpu_device_name()\nelse:\n    device_name = 'cpu:0'\n\nwith tf.device(device_name):\n    model = build_model()\n\nmodel.summary()","metadata":{"execution":{"iopub.status.busy":"2021-08-29T04:25:28.22704Z","iopub.execute_input":"2021-08-29T04:25:28.227704Z","iopub.status.idle":"2021-08-29T04:25:30.658069Z","shell.execute_reply.started":"2021-08-29T04:25:28.22765Z","shell.execute_reply":"2021-08-29T04:25:30.657013Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# モデルの学習\nmodel.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=config['learning_rate']),\n              loss=tf.keras.losses.BinaryCrossentropy(from_logits=False),\n              metrics=['accuracy'])\n\nearly_stopping = tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=3)\nmodel_checkpoint = tf.keras.callbacks.ModelCheckpoint(\n    filepath=config['output_path'],\n    save_weights_only=True,\n    monitor='val_loss',\n    mode='min',\n    save_best_only=True)\n\nhistory = model.fit(train_ds, epochs=config['num_epochs'],\n                    validation_data=valid_ds, shuffle=True,\n                    callbacks=[early_stopping, model_checkpoint],\n                    )\n\ntrain_losses = history.history['loss']\nvalid_losses = history.history['val_loss']\nnum_epochs = config['num_epochs']","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 4. Prediction ","metadata":{}},{"cell_type":"code","source":"proba = model.predict(test_ds, batch_size=16, verbose=1)\nproba\n\ntest_df['prediction'] = proba\nsample_df['MGMT_value'] = test_df['prediction']\nsample_df","metadata":{"execution":{"iopub.status.busy":"2021-08-28T15:19:50.249478Z","iopub.execute_input":"2021-08-28T15:19:50.250034Z","iopub.status.idle":"2021-08-28T15:19:51.795692Z","shell.execute_reply.started":"2021-08-28T15:19:50.249977Z","shell.execute_reply":"2021-08-28T15:19:51.794689Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"sample_df.to_csv(\"submission.csv\", index=False)","metadata":{"execution":{"iopub.status.busy":"2021-08-28T15:19:57.56055Z","iopub.execute_input":"2021-08-28T15:19:57.563334Z","iopub.status.idle":"2021-08-28T15:19:57.577836Z","shell.execute_reply.started":"2021-08-28T15:19:57.563258Z","shell.execute_reply":"2021-08-28T15:19:57.576145Z"},"trusted":true},"execution_count":null,"outputs":[]}]}