LoginSignup

This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

More than 1 year has passed since last update.

Tensorflow.Keras、Unetを用いてMRI画像の脳腫瘍をセグメンテーション

Last updated at Posted at 2021-12-03

筆者背景

・生命科学、医学分野の研究者です。
・非エンジニアです。
・プログラミング初心者です。pythonでAI、Bioinfomatics 勉強中です。

試したこと

以下条件下で脳MRI画像上での脳腫瘍のセグメンテーション実装を試みました。
条件1:KaggleやGitHubのコードを用いる。
条件2:Unet、Tensorflow (Keras)を用いる。
条件3:Web アプリとして公開する。
(※セグメンテーションAI開発には人間が手作業でマーキングしたアノテーションデータが必要です。一般にアノテーションデータ作成には膨大な労力を要します。ここではアノテーションの方法については議論せず、予めアノテーションされた公開データセットをダウンロードして利用します。アノテーションに関連した参考リンクは文末に記載します。)

公開

・アプリをWeb上に公開しました。
https://braintumorseg.herokuapp.com/
・セグメンテーションAI開発コードはipynbとしてGitHubに公開しました。
https://github.com/id-f/brain_mri_segmentation/blob/main/submit_segmodel_brainMRI.ipynb
・Web公開したアプリのソースコードもGitHubに公開しました。
https://github.com/id-f/brain_mri_segmentation

Webアプリ実行

以下はWebアプリを使用時のスクリーンショットです。
上記リンクに脳腫瘍のある脳のMRI画像をアップし、Submitボタンを押した後に現れるAIによってセグメンテーションされた画面です。

scs.png

なぜKaggleやGitHubのコードを用いたか?

アプリ作成には「KaggleやGitHubを参考にすると良い」とよく耳にしますが、私のような初心者にとっては、それらを利用するハードルが非常に高いと感じていました。ハードルが高い理由は人それぞれで色々とあると思われますが、初心者にとっては「KaggleやGitHubがなんなのかわからない。」「そもそもプログラミング自体わからない」などが挙げられると思います。私はKaggleやGitHubの苦手意識を解消すべく勉強しました。Pythonを勉強し、KaggleやGitHubについて調べ、慣れ親しむ努力をしました。しかしながら、それら学習後にも依然として立ちはだかるハードルがありました。それは、「サイトに書いてあるコードを組み合わせて使うとエラー必発し、エラーから抜け出せない。」です。今回はエンジニアの方にご指導いただく機会がありましたので、備忘録を兼ねて学んだことを記述しています。

なぜセグメンテーションなのか?

・私自身の研究用のセグメンテーションアプリ開発のためのワンステップ(要は練習)。
・セグメンテーションと分類(Classification)の違いについては、こちらのサイトが参考になりました。https://ys0510.hatenablog.com/entry/segmentation_summary

データセット

kaggleに公開されているデータセット ”Brain MRI segmentation” を使用しました。
https://www.kaggle.com/mateuszbuda/lgg-mri-segmentation
(画像元は ”The images were obtained from The Cancer Imaging Archive (TCIA).” です。)
このデータセットを用いた理由は以下です。
・(私が興味のある)医療用画像であるため。
・Kaggle内で公開されているため。
・Kaggle内で投稿が多く参考にできるソースコードが多かったため。

使用した参考コード

以下の1と2のコードを合体させて実装しました。
1のUnetのモデルの代わりに、2から流用したモデルを使用しました。

1、メインソースコード:KAGGLEに公開されているNOTEBOOKです。
https://www.kaggle.com/monkira/brain-mri-segmentation-using-unet-keras/notebook
このノートブックを選択した理由は以下です。
・Unetを使用している。
(なぜUnetなのか? 「まず初心者は有名かつセグメンテーションに優れているUnetを使うべき」という発想に基づきました。)
・kerasを使用している。(Kerasはpytorchやtensorflowと比して初心者向き聞いていたためです。)
・Generatorが理解しやすい形で実装されるため。

コードはこちら (クリックで開くと非常に長いコードが表示されます)
import os
import random
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
plt.style.use("ggplot")
%matplotlib inline

import cv2
from tqdm import tqdm_notebook, tnrange
from glob import glob
from itertools import chain
from skimage.io import imread, imshow, concatenate_images
from skimage.transform import resize
from skimage.morphology import label
from sklearn.model_selection import train_test_split

import tensorflow as tf
from skimage.color import rgb2gray
from tensorflow.keras import Input
from tensorflow.keras.models import Model, load_model, save_model
from tensorflow.keras.layers import Input, Activation, BatchNormalization, Dropout, Lambda, Conv2D, Conv2DTranspose, MaxPooling2D, concatenate
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint

from tensorflow.keras import backend as K
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint
#Set Parameters
im_width = 256
im_height = 256

Load image's path and mask's path

train_files = []
mask_files = glob('../input/lgg-mri-segmentation/kaggle_3m/*/*_mask*')

for i in mask_files:
    train_files.append(i.replace('_mask',''))

print(train_files[:10])
print(mask_files[:10])

Data Visualization

#Lets plot some samples
rows,cols=3,3
fig=plt.figure(figsize=(10,10))
for i in range(1,rows*cols+1):
    fig.add_subplot(rows,cols,i)
    img_path=train_files[i]
    msk_path=mask_files[i]
    img=cv2.imread(img_path)
    img=cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
    msk=cv2.imread(msk_path)
    plt.imshow(img)
    plt.imshow(msk,alpha=0.4)
plt.show()

Create data frame and split data on train set, validation set and test set

df = pd.DataFrame(data={"filename": train_files, 'mask' : mask_files})
df_train, df_test = train_test_split(df,test_size = 0.1)
df_train, df_val = train_test_split(df_train,test_size = 0.2)
print(df_train.values.shape)
print(df_val.values.shape)
print(df_test.values.shape)

Data genertator, data augmentation and adjust data

# From: https://github.com/zhixuhao/unet/blob/master/data.py
def train_generator(data_frame, batch_size, aug_dict,
        image_color_mode="rgb",
        mask_color_mode="grayscale",
        image_save_prefix="image",
        mask_save_prefix="mask",
        save_to_dir=None,
        target_size=(256,256),
        seed=1):
    '''
    can generate image and mask at the same time use the same seed for
    image_datagen and mask_datagen to ensure the transformation for image
    and mask is the same if you want to visualize the results of generator,
    set save_to_dir = "your path"
    '''
    image_datagen = ImageDataGenerator(**aug_dict)
    mask_datagen = ImageDataGenerator(**aug_dict)

    image_generator = image_datagen.flow_from_dataframe(
        data_frame,
        x_col = "filename",
        class_mode = None,
        color_mode = image_color_mode,
        target_size = target_size,
        batch_size = batch_size,
        save_to_dir = save_to_dir,
        save_prefix  = image_save_prefix,
        seed = seed)

    mask_generator = mask_datagen.flow_from_dataframe(
        data_frame,
        x_col = "mask",
        class_mode = None,
        color_mode = mask_color_mode,
        target_size = target_size,
        batch_size = batch_size,
        save_to_dir = save_to_dir,
        save_prefix  = mask_save_prefix,
        seed = seed)

    train_gen = zip(image_generator, mask_generator)

    for (img, mask) in train_gen:
        img, mask = adjust_data(img, mask)
        yield (img,mask)

def adjust_data(img,mask):
    img = img / 255
    mask = mask / 255
    mask[mask > 0.5] = 1
    mask[mask <= 0.5] = 0

    return (img, mask)

Define loss function and metrics

smooth=100

def dice_coef(y_true, y_pred):
    y_truef=K.flatten(y_true)
    y_predf=K.flatten(y_pred)
    And=K.sum(y_truef* y_predf)
    return((2* And + smooth) / (K.sum(y_truef) + K.sum(y_predf) + smooth))

def dice_coef_loss(y_true, y_pred):
    return -dice_coef(y_true, y_pred)

def iou(y_true, y_pred):
    intersection = K.sum(y_true * y_pred)
    sum_ = K.sum(y_true + y_pred)
    jac = (intersection + smooth) / (sum_ - intersection + smooth)
    return jac

def jac_distance(y_true, y_pred):
    y_truef=K.flatten(y_true)
    y_predf=K.flatten(y_pred)

    return - iou(y_true, y_pred)

Define Unet

def conv_bn_relu_block(num_kernel,input_layer):
    x = Conv2D(num_kernel, (3,3), padding='same')(input_layer)
    x = BatchNormalization(axis=3)(x)
    x = Activation('relu')(x)
    x = Conv2D(num_kernel, (3,3), padding='same')(x)
    x = BatchNormalization(axis=3)(x)
    x = Activation('relu')(x)
    return x
def maxpool_conv_bn_relu_block(num_kernel, input_layer):
    x = MaxPooling2D(pool_size=(2,2))(input_layer)
    x = conv_bn_relu_block(num_kernel,x)
    return x
def upconv_conv_bn_relu_block(num_kernel,input_layer,concat_layer):
    x = concatenate([Conv2DTranspose(num_kernel, (2, 2), strides=(2, 2), padding='same')(input_layer), concat_layer], axis=3)
    x = conv_bn_relu_block(num_kernel,x)
    return x

def unet(input_size = (256,256,3)):
    inputs = Input(input_size)

    conv_bn_relu_block_64 = conv_bn_relu_block(64,inputs)
    maxpool_conv_bn_relu_128 = maxpool_conv_bn_relu_block(128,conv_bn_relu_block_64)
    maxpool_conv_bn_relu_256 = maxpool_conv_bn_relu_block(256,maxpool_conv_bn_relu_128)
    maxpool_conv_bn_relu_512 = maxpool_conv_bn_relu_block(512,maxpool_conv_bn_relu_256)
    maxpool_conv_bn_relu_1024 = maxpool_conv_bn_relu_block(1024,maxpool_conv_bn_relu_512)

    upconv_conv_bn_relu_512 = upconv_conv_bn_relu_block(512,maxpool_conv_bn_relu_1024,maxpool_conv_bn_relu_512)
    upconv_conv_bn_relu_256 = upconv_conv_bn_relu_block(256,upconv_conv_bn_relu_512,maxpool_conv_bn_relu_256)
    upconv_conv_bn_relu_128 = upconv_conv_bn_relu_block(128,upconv_conv_bn_relu_256,maxpool_conv_bn_relu_128)
    upconv_conv_bn_relu_64 = upconv_conv_bn_relu_block(64,upconv_conv_bn_relu_128,conv_bn_relu_block_64)

    last_conv = Conv2D(1, (1, 1), activation='sigmoid')(upconv_conv_bn_relu_64)

    return Model(inputs=[inputs], outputs=[last_conv])


Training

EPOCHS = 150
BATCH_SIZE = 16
learning_rate = 1e-4
train_generator_args = dict(rotation_range=0.2,
                            width_shift_range=0.05,
                            height_shift_range=0.05,
                            shear_range=0.05,
                            zoom_range=0.05,
                            horizontal_flip=True,
                            fill_mode='nearest')
train_gen = train_generator(df_train, BATCH_SIZE,
                                train_generator_args,
                                target_size=(im_height, im_width))

test_gener = train_generator(df_val, BATCH_SIZE,
                                dict(),
                                target_size=(im_height, im_width))

model = unet(input_size=(im_height, im_width, 3))



decay_rate = learning_rate / EPOCHS
opt = Adam(lr=learning_rate, beta_1=0.9, beta_2=0.999, epsilon=None, decay=decay_rate, amsgrad=False)
model.compile(optimizer=opt, loss=dice_coef_loss, metrics=["binary_accuracy", iou, dice_coef])

callbacks = [ModelCheckpoint('unet_brain_mri_seg.hdf5', verbose=1, save_best_only=True)]

history = model.fit(train_gen,
                    steps_per_epoch=len(df_train) / BATCH_SIZE, 
                    epochs=EPOCHS, 
                    callbacks=callbacks,
                    validation_data = test_gener,
                    validation_steps=len(df_val) / BATCH_SIZE)
a = history.history


list_trainjaccard = a['iou']
list_testjaccard = a['val_iou']

list_trainloss = a['loss']
list_testloss = a['val_loss']
plt.figure(1)
plt.plot(list_testloss, 'b-',label = "Val loss")
plt.plot(list_trainloss,'r-', label = "Train loss")
plt.xlabel('Iteration')
plt.ylabel('Loss')
plt.title('Loss graph', fontsize = 15)
plt.figure(2)
plt.plot(list_trainjaccard, 'r-',label = "Train IOU")
plt.plot(list_testjaccard, 'b-',label = "Val IOU" )
plt.xlabel('Iteration')
plt.ylabel('Accuracy')
plt.title('Accuracy graph', fontsize = 15)
plt.show()
model = load_model('unet_brain_mri_seg.hdf5', custom_objects={'dice_coef_loss': dice_coef_loss, 'iou': iou, 'dice_coef': dice_coef})
test_gen = train_generator(df_test, BATCH_SIZE,
                                dict(),
                                target_size=(im_height, im_width))
results = model.evaluate(test_gen, steps=len(df_test) / BATCH_SIZE)
print("Test lost: ",results[0])
print("Test IOU: ",results[2])
print("Test Dice Coefficent: ",results[3])
for i in range(30):
    index=np.random.randint(1,len(df_test.index))
    img = cv2.imread(df_test['filename'].iloc[index])
    img = cv2.resize(img ,(im_height, im_width))
    img = img / 255
    img = img[np.newaxis, :, :, :]
    pred=model.predict(img)

    plt.figure(figsize=(12,12))
    plt.subplot(1,3,1)
    plt.imshow(np.squeeze(img))
    plt.title('Original Image')
    plt.subplot(1,3,2)
    plt.imshow(np.squeeze(cv2.imread(df_test['mask'].iloc[index])))
    plt.title('Original Mask')
    plt.subplot(1,3,3)
    plt.imshow(np.squeeze(pred) > .5)
    plt.title('Prediction')
    plt.show()

2、サブのソースコード:GitHubに公開されているコードで、segmentation_models というライブラリーです。
https://github.com/qubvel/segmentation_models
本モデルを選択した理由は以下です。
・複数アーキテクチャーと複数エンコーダ部を簡単に(1行で)使用できそうだから。
・GitHubのスター数が多いから。
・日本語解説サイトがあるから。https://farml1.com/segmentation-models/
以下でUnetとSegmentation modelsについて解説します。

Unetついて

UnetとはU字型構造のアーキテクチャーでConvolution Neural network(畳込みニューラルネットワーク)の一種です。そのU字構造からUnetと呼ばれます。生物医学で画像のセグメンテーションを行うために2015年に発表されたネットワークです。以下のような特徴を持ちます。
・全結合層を持たず畳込み層で構成される。
・Encoder-Decoder構造を持つ。U字型の左半分をエンコーダー、右側をデコーダーと呼ぶ。
・U字に直列に繋がれてるだけではなく、Expansive pathを有する。Contracting pathはU字の直列に繋がれた部分。Expansive pathはU字を橋渡しするような並列に繋がれた部分のことを指す。Expansive pathはスキップコネクションとも呼ばれている。この構造は同じ大きさの画像の復元(デコーディング)する必要があるセグメンテーションに役立つと考えられている。

詳細は理解できませんが、以上特徴からUnetはセグメンテーションに有利な畳込みニューラルネットワークと言われています。
また、エンコーダー部分としてEfficientNetやResnetなどのBackbornを使用することができます。その実装が簡単に行えるのが、今回使用したSegmentation modelsというライブラリーです。

https://www.skillupai.com/blog/tech/segmentation2/
・論文URL :https://arxiv.org/abs/1505.04597) 
https://www.acceluniverse.com/blog/developers/2019/11/u-net.html
https://en.wikipedia.org/wiki/U-Net
https://blog.negativemind.com/2019/03/15/semantic-segmentation-by-u-net/
https://www.acceluniverse.com/blog/developers/2019/11/u-net.html

環境

・Kaggle notebook 使用
・Google collaboratory 使用

実装手順

1、Kaggle上でそのまま実行してみる
2、Kaggle Notebookでコード合体(エラーに対処する)
2-1 Tesorflowのversionを揃える
2-2 不要コードの除去
2-3 代替コード移植
3、Google collaboratory上実行可能にする。
4、実行結果確認とトレーニング済み判別器のファイル出力(”XXX.hdf5”)
5、”XXX.hdf5”ファイル、FlaskとHerokuを用いてWebアプリとして公開する。

手順1 Kaggle上でそのまま実行してみる

https://www.kaggle.com/monkira/brain-mri-segmentation-using-unet-keras/notebook
を開き、右上の "copy and edit" ボタンを押し、新たに開かれたノートブックですべてのセルを実行します。
しかし、実行に長時間かかるため、時間短縮のためコード内のEPOCHS = 150Epochs = 2 などに大幅に減らして実行してみます。

手順2 Kaggle notebookでコード合体(エラーに対処する)

実行できることがわかったら、今度は別のコードからモデルを移植する。

2-1 Tesorflowのversionを揃える

まずはversionを揃えることが必要です。
ノートブック冒頭にライブラリをインポートするセルブロックがあります。

ライブラリインポート
import os
import random
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
plt.style.use("ggplot")
%matplotlib inline

import cv2
from tqdm import tqdm_notebook, tnrange
from glob import glob
from itertools import chain
from skimage.io import imread, imshow, concatenate_images
from skimage.transform import resize
from skimage.morphology import label
from sklearn.model_selection import train_test_split

import tensorflow as tf
from skimage.color import rgb2gray
from tensorflow.keras import Input
from tensorflow.keras.models import Model, load_model, save_model
from tensorflow.keras.layers import Input, Activation, BatchNormalization, Dropout, Lambda, Conv2D, Conv2DTranspose, MaxPooling2D, concatenate
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint

from tensorflow.keras import backend as K
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint

ここは 「インポートする「だけ」であるためセルを実行するだけの簡単な部分」だと思って流していたのですが、この後にこの部分が原因で大いにハマりました。問題点はTensorflowのバージョン違いです。
インストールされているtensorflownバージョンの確認方法は tensorflow.__version__ です。

import tensorflow
tensorflow.__version__
#実行結果
>>>'2.7.0'

のようになります。2021年12月3日現在では、tensorflowをインストールするとバージョンが '2.7.0' となります。これが後のエラーの原因となりました。
ソース元のkaggleのnotebook上でtensorflowのバージョンを確認すると、

import tensorflow
tensorflow.__version__
>>>'2.2.0'

と表示されます。tensorflowのバージョンは'2.2.0'です。
バージョン'2.2.0'をインストールするコードは

pip install tensorflow=='2.2.0'

です。このブログ執筆時点では頭が整理されておりますが、実装過程で解決するまでに多くの時間を浪費しました。今回の個人的教訓は「人様の書いたコードを使用させていただく時には、(感謝しながら)使用ライブラリのバージョンを一致されましょう」です。

手順2-2 不要コードの除去

ノートブック上の"Define Unet"のブロックの以下のコードをすべて削除(またはコメントアウト)します。

コードはこちら (クリックでコードが表示されます)
def unet(input_size=(256,256,3)):
    inputs = Input(input_size)

    conv1 = Conv2D(64, (3, 3), padding='same')(inputs)
    bn1 = Activation('relu')(conv1)
    conv1 = Conv2D(64, (3, 3), padding='same')(bn1)
    bn1 = BatchNormalization(axis=3)(conv1)
    bn1 = Activation('relu')(bn1)
    pool1 = MaxPooling2D(pool_size=(2, 2))(bn1)

    conv2 = Conv2D(128, (3, 3), padding='same')(pool1)
    bn2 = Activation('relu')(conv2)
    conv2 = Conv2D(128, (3, 3), padding='same')(bn2)
    bn2 = BatchNormalization(axis=3)(conv2)
    bn2 = Activation('relu')(bn2)
    pool2 = MaxPooling2D(pool_size=(2, 2))(bn2)

    conv3 = Conv2D(256, (3, 3), padding='same')(pool2)
    bn3 = Activation('relu')(conv3)
    conv3 = Conv2D(256, (3, 3), padding='same')(bn3)
    bn3 = BatchNormalization(axis=3)(conv3)
    bn3 = Activation('relu')(bn3)
    pool3 = MaxPooling2D(pool_size=(2, 2))(bn3)

    conv4 = Conv2D(512, (3, 3), padding='same')(pool3)
    bn4 = Activation('relu')(conv4)
    conv4 = Conv2D(512, (3, 3), padding='same')(bn4)
    bn4 = BatchNormalization(axis=3)(conv4)
    bn4 = Activation('relu')(bn4)
    pool4 = MaxPooling2D(pool_size=(2, 2))(bn4)

    conv5 = Conv2D(1024, (3, 3), padding='same')(pool4)
    bn5 = Activation('relu')(conv5)
    conv5 = Conv2D(1024, (3, 3), padding='same')(bn5)
    bn5 = BatchNormalization(axis=3)(conv5)
    bn5 = Activation('relu')(bn5)

    up6 = concatenate([Conv2DTranspose(512, (2, 2), strides=(2, 2), padding='same')(bn5), conv4], axis=3)
    conv6 = Conv2D(512, (3, 3), padding='same')(up6)
    bn6 = Activation('relu')(conv6)
    conv6 = Conv2D(512, (3, 3), padding='same')(bn6)
    bn6 = BatchNormalization(axis=3)(conv6)
    bn6 = Activation('relu')(bn6)

    up7 = concatenate([Conv2DTranspose(256, (2, 2), strides=(2, 2), padding='same')(bn6), conv3], axis=3)
    conv7 = Conv2D(256, (3, 3), padding='same')(up7)
    bn7 = Activation('relu')(conv7)
    conv7 = Conv2D(256, (3, 3), padding='same')(bn7)
    bn7 = BatchNormalization(axis=3)(conv7)
    bn7 = Activation('relu')(bn7)

    up8 = concatenate([Conv2DTranspose(128, (2, 2), strides=(2, 2), padding='same')(bn7), conv2], axis=3)
    conv8 = Conv2D(128, (3, 3), padding='same')(up8)
    bn8 = Activation('relu')(conv8)
    conv8 = Conv2D(128, (3, 3), padding='same')(bn8)
    bn8 = BatchNormalization(axis=3)(conv8)
    bn8 = Activation('relu')(bn8)

    up9 = concatenate([Conv2DTranspose(64, (2, 2), strides=(2, 2), padding='same')(bn8), conv1], axis=3)
    conv9 = Conv2D(64, (3, 3), padding='same')(up9)
    bn9 = Activation('relu')(conv9)
    conv9 = Conv2D(64, (3, 3), padding='same')(bn9)
    bn9 = BatchNormalization(axis=3)(conv9)
    bn9 = Activation('relu')(bn9)

    conv10 = Conv2D(1, (1, 1), activation='sigmoid')(bn9)

    return Model(inputs=[inputs], outputs=[conv10])

以下コードブロックも削除またはコメントアウトして、

model = unet()
model.summary()

Trainingのセルブロックの以下も削除またはコメントアウトします

#model = unet(input_size=(im_height, im_width, 3))

次に、以上で削除したコードの替わりになるコードを足していきます。

手順2-3 代替コード移植

以下のコードを Define Unetのブロックの先頭に挿入します。

pip install segmentation_models
import segmentation_models as sm
from tensorflow.keras import backend as K
K.set_image_data_format('channels_last')
BACKBONE = 'efficientnetb0'
CLASSES = ["MASK"]
preprocess_input = sm.get_preprocessing(BACKBONE)
# define network parameters
n_classes = 1 if len(CLASSES) == 1 else (len(CLASSES) + 1)  # case for binary and multiclass segmentation
activation = 'sigmoid' if n_classes == 1 else 'softmax'

#create model
model = sm.Unet(BACKBONE, input_shape=(im_height, im_width, 3), classes=n_classes, activation=activation)

ここではまず segmentation_modelsをインストールし、インポートします。
segmentation_modelsの使い方は上述の公式ページや日本語解説サイトに記載あります。
その他必要な引数を上記ページを参照にしながら設定しています。
ここで使用するトレーニング済のモデルとしては、'efficientnetb0'を利用しました。

合体させたコードを実行し、つつがなくコードが実行されることを確かめます。

手順3 Google collaboratory上で実行可能にする。

さらに作成したコードをGoogle collaboratory上で実行可能にします。
そのためには、パスやAPIの設定が必要となります。

KaggleにGoogle collaboratoryとの連携方法が記載されています。
https://www.kaggle.com/general/74235

さらにここでは、Google collaboratoryのノードブック上のコードを再実行する際の手間を省くために、
・APItokenをgoogle drive内にtokenを保存し、
・Google drive とmountしたGoogle collaboratoryのノートブック内で、コマンドラインを用いてgoogle drive内にtokenを保存し、それを利用する方法を取っています。

Google collaboratoryのノートブックの冒頭の以下のコードが該当部分になります。

! pip install -q kaggle

! mkdir ~/.kaggle
! cp /content/drive/MyDrive/kaggle.json ~/.kaggle/
! chmod 600 ~/.kaggle/kaggle.json
! kaggle datasets download -d mateuszbuda/lgg-mri-segmentation
! mkdir ./train/
! unzip lgg-mri-segmentation.zip -d train

手順4 実行結果確認とファイル出力(.hdf5ファイル)

4-1 実行結果確認

以下は Epoch数20 でトレーニングを行なった際のグラフです。
Loss graph とはLoss関数=損失関数を表したグラフのことです。損失関数とは誤差関数のことです。機械学習分野では損失関数と呼ばれていますが、誤差関数と呼んだ方が私は直感的でわかりやすいです。誤差とは(実際の測定値ーモデルが出力した予測値)のことです。線形回帰の様な単純なモデルを例として考えるとわかりやすいですが、予測値は予測モデルとして、関数の形(プログラミングにおける関数ではなく数学における関数)をとっていますので、(実際の測定値ーモデルが出力した予測値)は関数となります。y - y^ のように記載されます。(実際の測定値ーモデルが出力した予測値)は小さければ小さいほど良いモデルということになりますので、誤差関数は小さくなれば小さくなるほど良いといことになります。
(この辺りの説明は 統計学が最強の学問である[数学編] が非常に分かり易かったです。)

以下のグラフはEpoch数が増えるに従って、Loss関数がどんどん減っており、Accuracyが上昇していることがわかり、学習がうまく実施されていることを示しています。なお、このLoss関数のコードの説明と背景に関しては、分量が多くなりすぎて、このブログには書ききれませんので、別途時間のある時に改めて投稿したいと思います。

スクリーンショット 2021-12-12 16.32.50.png

スクリーンショット 2021-12-12 16.32.57.png

4-2 トレーニング済み判別器のファイル出力(.hdf5ファイル)

前述したように以下コードで学習させたモデル(パラメーター重み)ファイルはカレントディレクトリ(ここではGooglecolabのデフォルトの一時フォルダ)に .hdfが保存されます。

callbacks = [ModelCheckpoint('unet_brain_mri_seg.hdf5', verbose=1, save_best_only=True)]

保存済みファイルは

model = load_model('unet_brain_mri_seg.hdf5', custom_objects={'dice_coef_loss': dice_coef_loss, 'iou': iou, 'dice_coef': dice_coef})

のように後に読み込むことで再利用が可能です。

手順5、.hdf5ファイル、FlaskとHerokuを用いてWebアプリとして公開。

ここではデプロイ詳細は省略します。Htmlファイルとpythonファイル(.py)の間で変数をやり取りする仕様などに不慣れで、Flaskの動作を理解するまでにかなりの時間がかかりました。またGitも不慣れで苦労しました。さらにWebブラウザ上で美しく表示するためには、HTMLやCSSの工夫(学習)が必要だとは思いましたが今回はその作業を行わずに見送りました。本過程でフロントエンド・バックエンドという言葉を知りました。そして、私が興味があるのは分野はバックエンドやデータ分析関連の知識/技術であることがわかりました。

その他ポイント

上記実践すればプログラムは動くと思われますが、その他学びがあった点について備忘録として記載します。
・メインのソースコードである、Kaggleノートのうち、generatorが肝と思います。Genetorと呼ばれる関数はreturnの代わりに yieldを用います。Genetorは本コードのみならず開発時一般で使用される方法(プログラム)ですが、画像AI関連においては、メモリ節約しながらバッチサイズにあわせてトレーニングデータ(およびテストデータ)を作成することができる優れたモノ(関数)のようです。ここでは、ImageDataGeneratorというインスタンスが作成され、そのメソッドの.flow_from_dataframeを用いており、その引数にはbatch_sizeも存在します。それをZipでまとめてFor分で回して、batch_sizeに応じた画像を取り出しています。(一般的に)Generatorとはなんぞや? ということはこちらの記事が参考になりました。https://qiita.com/keitakurita/items/5a31b902db6adfa45a70

def train_generator(data_frame, batch_size, aug_dict,
        image_color_mode="rgb",
        mask_color_mode="grayscale",
        image_save_prefix="image",
        mask_save_prefix="mask",
        save_to_dir=None,
        target_size=(256,256),
        seed=1):
 image_datagen = ImageDataGenerator(**aug_dict)
 mask_datagen = ImageDataGenerator(**aug_dict)
 image_generator = image_datagen.flow_from_dataframe(
        data_frame,
        x_col = "filename",
        class_mode = None,
        color_mode = image_color_mode,
        target_size = target_size,
        batch_size = batch_size,
        save_to_dir = save_to_dir,
        save_prefix  = image_save_prefix,
        seed = seed)

 mask_generator = mask_datagen.flow_from_dataframe(
        data_frame,
        x_col = "mask",
        class_mode = None,
        color_mode = mask_color_mode,
        target_size = target_size,
        batch_size = batch_size,
        save_to_dir = save_to_dir,
        save_prefix  = mask_save_prefix,
        seed = seed)

 train_gen = zip(image_generator, mask_generator)

 for (img, mask) in train_gen:
      img, mask = adjust_data(img, mask)
      yield (img,mask)

・トレーニングされたモデルは tesorflowの ModelCheckpointによって保存されます。

callbacks = [ModelCheckpoint('unet_brain_mri_seg.hdf5', verbose=1, save_best_only=True)]

.全体のコードはGitHubに公開しています。.ipynbとしてノートブック形式になっており実行結果も表示されます。
https://github.com/id-f/brain_mri_segmentation/blob/main/submit_segmodel_brainMRI.ipynb

まとめ

KaggleとGitHubのコードを利用して、Unetを用いてMRI画像上の脳腫瘍のセグメンテーションの実装を行いました。
また、Flask、Herokuを利用して、Webアプリとして公開しました。
「畳込みニューラルネットワークを用いて、pythonでセグメンテーションを実装する」が本案件の本丸だったのですが、学習過程ではGoogle colab, コマンドプロンプト、Kaggle, Kaggle Notebook, jupyter notebook, GitHub, Flask, Heroku など、そもそも存在すらほとんど知らなかったことが多数あり、それらの学習に長い時間を要しました。
ご指導いただきましたエンジニアの方には大変感謝をしています。

多くの学びがありましたが、個人的教訓を1、2点に絞って挙げるとすれば、
・人様の書いたコードを使用させていただく時には、(感謝しながら)使用ライブラリのバージョンを一致させるべし。
・KaggleやGitHubの公開コードは出来る限り利活用すべし。
になります。長文読んでいただき、ありがとうございました。

参考サイト、その他役立つリンク

Awesome Semantic Segmentation GitHub
https://github.com/mrgloom/awesome-semantic-segmentation
Paper with code. Semantic Segmentation
https://paperswithcode.com/task/semantic-segmentation
https://knossos.app/#FeaturesSection
https://www.youtube.com/watch?v=NJNvXEYa_gM

セグメンテーションのアノテーション関連サイト。
https://www.v7labs.com/blog/best-image-annotation-tools (無料/有料まとめ)
https://demura.net/deeplearning/16931.html  CVAT
https://deecode.net/?p=735
https://engineering.mobalab.net/2020/07/27/data-labeling-by-label-studio/
https://github.com/diffgram/diffgram
https://anthony-sarkis.medium.com/the-5-best-ai-data-annotation-platforms-for-machine-learning-2021-ec17c15142f3
https://github.com/wkentaro/labelme

セグメンテーションと分類(Classification)の違いについて。
https://ys0510.hatenablog.com/entry/segmentation_summary

UNetの解説
https://www.skillupai.com/blog/tech/segmentation2/
・論文URL :https://arxiv.org/abs/1505.04597) 
https://www.acceluniverse.com/blog/developers/2019/11/u-net.html
https://en.wikipedia.org/wiki/U-Net
https://blog.negativemind.com/2019/03/15/semantic-segmentation-by-u-net/
https://www.acceluniverse.com/blog/developers/2019/11/u-net.html

Gitのわかりやすい解説
https://atmarkit.itmedia.co.jp/ait/articles/2005/21/news023.html

機会学習関連の数学の基礎を学べる本です。数学が苦手な人にも数学がわかるように書いてあると思います。良書です。統計学が最強の学問である[数学編]

0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up