大阪大学医学部 Python会 (情報医科学研究会)

Now is better than never.

転移学習とCAM

2019-06-28(Fri) - Posted by 味岡 in 技術ブログ    tag:Machine Learning

Contents

    こんにちは。神大の味岡です。一年くらい前 (編注: 原記事が執筆された2019年6月頃のこと) にdeep learningを使用した画像分類をしていたので、それについて書きたいと思います。サンプルとして、Kaggleのdog vs catを用いました。 とてもありがちな内容で恐縮ですがよろしくお願いします。

    転移学習とは

    deep learningには、大量のデータが必要とよく言われますよね。しかし、いつも十分なサンプルデータが得られるとは限りません。そこで、既に大量のデータで学習して得られたモデルを活用してそこに修正を加えてオリジナルのモデルを作るという技術が作られました。いうなれば既存のモデルのチューニングするようなイメージです。層の最後のみを調整するのをtransfer learning、層の全体を調整するのをfine tuningというそうです。 CNNによる画像分類では、転移学習はよく使用されます。なぜなら画像分類で用いるCNNでは、入力に近い層は単純なパターンを認識するもので、これはどんな画像にも適用しやすいからです。 今回は既存のモデルとしては有名なVGG16を使用しました。これに全結合層を追加して、最終層に犬と猫の2クラス分類を出力させました。

    環境設定

    環境は以下です。

    Ubuntu 16.04

    CUDA 9.0

    Anaconda

    Python 3.5

    tensorflow-gpu==1.10.0

    Keras==2.2.2

    画像系のDeep Learningは処理が重いので、GPUを活用するのがおすすめです。

    使用したデータ

    Kaggleのdog vs catです。https://www.kaggle.com/c/dogs-vs-cats/data

    コード

    以下は転移学習のコードです。パラメーター設定の部分を変えれば、様々な画像の分類に使えるようにしました。パラメータの[#training dataの場所]と[#validation dataの場所]には、分類された画像を入れたフォルダが入ります。今回は「dog」フォルダと「cat」フォルダにそれぞれ犬、猫の画像を入れました。trainingにはそれぞれ1000枚ずつ、validationにはそれぞれ400枚ずつ入れました。

    追記:VGG16の入力形式に合わせて、ImageGeneratorの部分をrescale=1.0/255ではなく、preprocessing_function=preprocess_input_vggにする例もあります。

    In [10]:
    # パッケージのインポート
    import warnings
    from keras.preprocessing import image
    import numpy as np
    import matplotlib.pyplot as plt
    from keras.applications.vgg16 import VGG16
    from keras.models import Sequential, Model
    from keras.layers import Dense, Dropout, Flatten, Input
    from keras import optimizers
    
    # warningを非表示にする
    warnings.filterwarnings('ignore') 
    
    # パラメーター設定
    train_dir = 'DogCat/train' #training dataの場所
    valid_dir = 'DogCat/valid' #validation dataの場所
    n_categories = 2 # 分類クラスの数
    training_start_layer = 18 # 解凍する層の場所がVGG16最後の一層だけなら18
    height = 256 # 画像のサイズ(縦)
    width = 256 # 画像のサイズ(横) 
    EPOCH = 30 # エポック数
    
    # モデル作成
    base_model=VGG16(weights='imagenet',include_top=False,
                     input_tensor=Input(shape=(height,width,3)))
    top_model = Sequential()
    top_model.add(Flatten(input_shape=base_model.output_shape[1:]))
    top_model.add(Dense(256, activation='relu'))
    top_model.add(Dropout(0.5))
    top_model.add(Dense(n_categories, activation='softmax'))
    model=Model(inputs=base_model.input,outputs=top_model(base_model.output))
    
    # 層の凍結
    for layer in base_model.layers[:training_start_layer]:
        layer.trainable=False
    
    # モデルの表示
    model.summary()
    
    # 学習設定
    model.compile(loss='categorical_crossentropy', optimizer=optimizers.SGD(lr=1e-4, momentum=0.9), metrics=['accuracy'])
    
    # 画像データ設定
    image_data_generator = image.ImageDataGenerator(rescale=1.0/255)
    train_data =image_data_generator.flow_from_directory(
        train_dir,
        target_size=(height, width),
        batch_size=32,
        class_mode="categorical",
        shuffle=False
    )
    image_data_generator = image.ImageDataGenerator(rescale=1.0/255)
    validation_data = image_data_generator.flow_from_directory(
        valid_dir,
        target_size=(height, width),
        batch_size=32,
        class_mode="categorical",
        shuffle=False
    )
    
    # 学習の実行
    history = model.fit_generator(
          train_data,
          steps_per_epoch=100,
          epochs=EPOCH,
          validation_data=validation_data,
          validation_steps=50,
          verbose=2)
    
    # パラメータ保存
    file_name = 'vgg16_transferlearning_weights'
    model.save(file_name+'.h5')
    
    _________________________________________________________________
    Layer (type)                 Output Shape              Param #   
    =================================================================
    input_3 (InputLayer)         (None, 256, 256, 3)       0         
    _________________________________________________________________
    block1_conv1 (Conv2D)        (None, 256, 256, 64)      1792      
    _________________________________________________________________
    block1_conv2 (Conv2D)        (None, 256, 256, 64)      36928     
    _________________________________________________________________
    block1_pool (MaxPooling2D)   (None, 128, 128, 64)      0         
    _________________________________________________________________
    block2_conv1 (Conv2D)        (None, 128, 128, 128)     73856     
    _________________________________________________________________
    block2_conv2 (Conv2D)        (None, 128, 128, 128)     147584    
    _________________________________________________________________
    block2_pool (MaxPooling2D)   (None, 64, 64, 128)       0         
    _________________________________________________________________
    block3_conv1 (Conv2D)        (None, 64, 64, 256)       295168    
    _________________________________________________________________
    block3_conv2 (Conv2D)        (None, 64, 64, 256)       590080    
    _________________________________________________________________
    block3_conv3 (Conv2D)        (None, 64, 64, 256)       590080    
    _________________________________________________________________
    block3_pool (MaxPooling2D)   (None, 32, 32, 256)       0         
    _________________________________________________________________
    block4_conv1 (Conv2D)        (None, 32, 32, 512)       1180160   
    _________________________________________________________________
    block4_conv2 (Conv2D)        (None, 32, 32, 512)       2359808   
    _________________________________________________________________
    block4_conv3 (Conv2D)        (None, 32, 32, 512)       2359808   
    _________________________________________________________________
    block4_pool (MaxPooling2D)   (None, 16, 16, 512)       0         
    _________________________________________________________________
    block5_conv1 (Conv2D)        (None, 16, 16, 512)       2359808   
    _________________________________________________________________
    block5_conv2 (Conv2D)        (None, 16, 16, 512)       2359808   
    _________________________________________________________________
    block5_conv3 (Conv2D)        (None, 16, 16, 512)       2359808   
    _________________________________________________________________
    block5_pool (MaxPooling2D)   (None, 8, 8, 512)         0         
    _________________________________________________________________
    sequential_3 (Sequential)    (None, 2)                 8389378   
    =================================================================
    Total params: 23,104,066
    Trainable params: 8,389,378
    Non-trainable params: 14,714,688
    _________________________________________________________________
    Found 2000 images belonging to 2 classes.
    Found 800 images belonging to 2 classes.
    Epoch 1/30
     - 17s - loss: 0.8113 - acc: 0.5922 - val_loss: 0.6723 - val_acc: 0.5150
    Epoch 2/30
     - 15s - loss: 0.7090 - acc: 0.5438 - val_loss: 0.6130 - val_acc: 0.7887
    Epoch 3/30
     - 15s - loss: 0.6118 - acc: 0.6569 - val_loss: 0.7511 - val_acc: 0.5100
    Epoch 4/30
     - 15s - loss: 0.6010 - acc: 0.6684 - val_loss: 0.5694 - val_acc: 0.6025
    Epoch 5/30
     - 15s - loss: 0.5420 - acc: 0.7178 - val_loss: 0.5487 - val_acc: 0.6400
    Epoch 6/30
     - 15s - loss: 0.5397 - acc: 0.7163 - val_loss: 0.5316 - val_acc: 0.6700
    Epoch 7/30
     - 15s - loss: 0.4887 - acc: 0.7703 - val_loss: 0.3981 - val_acc: 0.8588
    Epoch 8/30
     - 15s - loss: 0.4757 - acc: 0.7741 - val_loss: 0.4215 - val_acc: 0.8125
    Epoch 9/30
     - 15s - loss: 0.4239 - acc: 0.8156 - val_loss: 0.3582 - val_acc: 0.8688
    Epoch 10/30
     - 15s - loss: 0.4478 - acc: 0.7878 - val_loss: 0.4404 - val_acc: 0.7788
    Epoch 11/30
     - 15s - loss: 0.3972 - acc: 0.8222 - val_loss: 0.3657 - val_acc: 0.8475
    Epoch 12/30
     - 15s - loss: 0.3573 - acc: 0.8478 - val_loss: 0.4623 - val_acc: 0.7562
    Epoch 13/30
     - 15s - loss: 0.3852 - acc: 0.8337 - val_loss: 0.4186 - val_acc: 0.7925
    Epoch 14/30
     - 15s - loss: 0.3926 - acc: 0.8222 - val_loss: 0.3776 - val_acc: 0.8275
    Epoch 15/30
     - 15s - loss: 0.3152 - acc: 0.8678 - val_loss: 0.3411 - val_acc: 0.8588
    Epoch 16/30
     - 15s - loss: 0.3048 - acc: 0.8778 - val_loss: 0.2952 - val_acc: 0.8888
    Epoch 17/30
     - 15s - loss: 0.3074 - acc: 0.8753 - val_loss: 0.2805 - val_acc: 0.8875
    Epoch 18/30
     - 15s - loss: 0.3000 - acc: 0.8772 - val_loss: 0.2737 - val_acc: 0.8900
    Epoch 19/30
     - 15s - loss: 0.2825 - acc: 0.8816 - val_loss: 0.2614 - val_acc: 0.9025
    Epoch 20/30
     - 15s - loss: 0.2913 - acc: 0.8781 - val_loss: 0.2694 - val_acc: 0.8988
    Epoch 21/30
     - 15s - loss: 0.3018 - acc: 0.8703 - val_loss: 0.2549 - val_acc: 0.9000
    Epoch 22/30
     - 15s - loss: 0.2554 - acc: 0.8981 - val_loss: 0.2783 - val_acc: 0.8838
    Epoch 23/30
     - 15s - loss: 0.2376 - acc: 0.9050 - val_loss: 0.2415 - val_acc: 0.9062
    Epoch 24/30
     - 15s - loss: 0.2659 - acc: 0.8888 - val_loss: 0.2460 - val_acc: 0.9075
    Epoch 25/30
     - 15s - loss: 0.2510 - acc: 0.9006 - val_loss: 0.2467 - val_acc: 0.9012
    Epoch 26/30
     - 15s - loss: 0.2355 - acc: 0.9059 - val_loss: 0.2334 - val_acc: 0.9125
    Epoch 27/30
     - 15s - loss: 0.2302 - acc: 0.9125 - val_loss: 0.2383 - val_acc: 0.9075
    Epoch 28/30
     - 15s - loss: 0.2116 - acc: 0.9184 - val_loss: 0.2534 - val_acc: 0.8950
    Epoch 29/30
     - 15s - loss: 0.2073 - acc: 0.9241 - val_loss: 0.2953 - val_acc: 0.8738
    Epoch 30/30
     - 15s - loss: 0.1977 - acc: 0.9219 - val_loss: 0.2257 - val_acc: 0.9100
    
    In [12]:
    # 図の作成
    plt.figure(figsize=(10,3))
    acc = history.history['acc']
    val_acc = history.history['val_acc']
    loss = history.history['loss']
    val_loss = history.history['val_loss']
    epochs = range(len(acc))
    plt.subplot(1,2,1)
    plt.plot(epochs, acc, 'bo', label='Training acc')
    plt.plot(epochs, val_acc, 'b', label='Validation acc')
    plt.title('Training and validation accuracy')
    plt.legend()
    plt.subplot(1,2,2)
    plt.plot(epochs, loss, 'bo', label='Training loss')
    plt.plot(epochs, val_loss, 'b', label='Validation loss')
    plt.title('Training and validation loss')
    plt.legend()
    plt.show()
    

    エポックが進むごとにaccuracyは上昇してlossが下がっていったので、学習できたと思います。

    CAM (Class Activation Map)

    せっかくなので、Grad-CAMでの判断根拠の可視化もやってみます。

    deep learningはブラックボックスと言われていますが、CNNではCAMで判断根拠の可視化を行いことができます。中でもGrad-CAMは有名です。

    コードはhttps://qiita.com/haru1977/items/45269d790a0ad62604b3 のものを使わせて頂きました。

    実行にはopencv-pythonもインストールする必要があります。

    In [27]:
    import pandas as pd
    import numpy as np
    import cv2
    from keras import backend as K
    from keras.preprocessing.image import array_to_img, img_to_array, load_img
    from keras.models import load_model
    
    K.set_learning_phase(1) #set learning phase
    
    def Grad_Cam(input_model, x, layer_name):
        '''
        Args:
           input_model: モデルオブジェクト
           x: 画像(array)
           layer_name: 畳み込み層の名前
    
        Returns:
           jetcam: 影響の大きい箇所を色付けした画像(array)
    
        '''
        # 前処理
        X = np.expand_dims(x, axis=0)
        X = X.astype('float32')
        preprocessed_input = X / 255.0
        # 予測クラスの算出
        predictions = model.predict(preprocessed_input)
        class_idx = np.argmax(predictions[0])
        class_output = model.output[:, class_idx]
        #  勾配を取得
        conv_output = model.get_layer(layer_name).output   # layer_nameのレイヤーのアウトプット
        grads = K.gradients(class_output, conv_output)[0]  # gradients(loss, variables) で、variablesのlossに関しての勾配を返す
        gradient_function = K.function([model.input], [conv_output, grads])  # model.inputを入力すると、conv_outputとgradsを出力する関数
        output, grads_val = gradient_function([preprocessed_input])
        output, grads_val = output[0], grads_val[0]
        # 重みを平均化して、レイヤーのアウトプットに乗じる
        weights = np.mean(grads_val, axis=(0, 1))
        cam = np.dot(output, weights)
        # 画像化してヒートマップにして合成
        cam = cv2.resize(cam, (256, 256), cv2.INTER_LINEAR) # 画像サイズは200で処理したので
        cam = np.maximum(cam, 0) 
        cam = cam / cam.max()
        jetcam = cv2.applyColorMap(np.uint8(255 * cam), cv2.COLORMAP_JET)  # モノクロ画像に疑似的に色をつける
        jetcam = cv2.cvtColor(jetcam, cv2.COLOR_BGR2RGB)  # 色をRGBに変換
        jetcam = (np.float32(jetcam) + x / 2)   # もとの画像に合成
        return jetcam
    

    Grad-CAMではモデルの層を指定して行うようです。 いくつかの猫の画像で判断根拠の可視化をやってみます。

    In [54]:
    input_dir = 'DogCat/valid/cats'
    layer = 'block5_conv3'
    
    i = 'cat.1002.jpg' #画像ファイルの名前
    x = img_to_array(load_img(input_dir + '/' + i, target_size=(256,256)))
    prob = model.predict(np.expand_dims(x, axis=0) / 255.0)
    print('猫の確率は'+ str(prob[0][0]))
    plt.subplot(1,2,1)
    plt.imshow(x / 255.0)
    plt.subplot(1,2,1)
    cam = Grad_Cam(model, x, layer)
    plt.subplot(1,2,2)
    plt.imshow(cam / cam.max())
    plt.show()
    
    猫の確率は0.99872714
    
    In [49]:
    i = 'cat.1005.jpg'
    x = img_to_array(load_img(input_dir + '/' + i, target_size=(256,256)))
    prob = model.predict(np.expand_dims(x, axis=0) / 255.0)
    print('猫の確率は'+ str(prob[0][0]))
    plt.subplot(1,2,1)
    plt.imshow(x / 255.0)
    plt.subplot(1,2,1)
    cam = Grad_Cam(model, x, layer)
    plt.subplot(1,2,2)
    plt.imshow(cam / cam.max())
    plt.show()
    
    猫の確率は0.9936093
    
    In [53]:
    i = 'cat.1010.jpg'
    x = img_to_array(load_img(input_dir + '/' + i, target_size=(256,256)))
    prob = model.predict(np.expand_dims(x, axis=0) / 255.0)
    print('猫の確率は'+ str(prob[0][0]))
    plt.subplot(1,2,1)
    plt.imshow(x / 255.0)
    plt.subplot(1,2,1)
    cam = Grad_Cam(model, x, layer)
    plt.subplot(1,2,2)
    plt.imshow(cam / cam.max())
    plt.show()
    
    猫の確率は0.95646036
    

    猫の顔や耳に反応していますね。 ただし、他の画像では変な場所に反応している画像もありました。

    感想

    deep learningライブラリの中でもKerasは初心者でも使いやすいライブラリだなと思いました。ImageDataGeneratorが便利ですね。

    CAMに関してはExplainable AI(説明可能なAI)に繋がる素晴らしい技術だと思いました。医療AIにも応用されていくのかなと思います。