あっぱれ日和、 - What a "Appare" day -

あっぱれ日和、 - What a "Appare" day -

通りすがりのしがない映画好きが綴る、モノづくりの記録や備忘録。そして、影響を受けた映画の記録。

機械学習初心者がDeep Learningを試してみる season CNN ~リベンジ編:第4話 改良~

はじめに、

こんにちは!
はい、前回の記事(以下)では、モデルの改良について考察を行いました。
apao-m-appare99.hatenablog.com
そして、本記事では実際に考察をもとに改良を試みていきます。
前回の記事でまとめましたが、今回改良のために実装するのは以下になります。

  • 入力データのピクセル数を増やす
  • モデルの層を増やす
  • 学習データを増やす

そして、今回も執筆するのはあくまでも機械学習ど素人のエンジニアもどきである私です。温かく見守ってください。
また間違っていたらご指摘のほどよろしくお願いいたします。

【目次】

改良策①:入力データのピクセル数を増やす

入力画像のピクセルは50×50でした。今回は100×100で実装してみます。それ以外のところは変更しません。コードは次の記事のimage_size = 50をimage_size = 100に買えるだけです。
apao-m-appare99.hatenablog.com
試しにどんな感じの画像か見てみましょう。
f:id:appare99:20200524000617p:plain
...画像が悪かったんだと思います。犬だとわかってないとわかんないですね。ただ50×50より鮮明になっていることは事実です。。。
ということで気になる学習結果を見てみましょう。

気になる結果

結果を以下に表示します。ちなみに前回の評価精度は約0.23です。

まずは評価精度は約0.208でした。
そして評価損失は約9.78でした。
...。
......。
いや、下がっとるやないか!
じゃあ一応精度と損失の推移プロットを以下に示します。(もう投げやり)
右が精度のプロット、左が損失の推移プロットになります。

f:id:appare99:20200524001518p:plainf:id:appare99:20200524001556p:plain

...もう次に行きますか。

改良策②:モデルの層を増やす

気を取り直して、次はモデルの層を増やします。
畳み込み層を深くすればするほど学習精度が上昇するので(と以下のページに書いてあったので)、
Deep Learning
畳み込み層を増やします。

前回までのモデル構成は以下のようになってました。(少し見にくいですがご容赦ください)


_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_1 (Conv2D) (None, 50, 50, 32) 896
_________________________________________________________________
activation_1 (Activation) (None, 50, 50, 32) 0
_________________________________________________________________
conv2d_2 (Conv2D) (None, 48, 48, 32) 9248
_________________________________________________________________
activation_2 (Activation) (None, 48, 48, 32) 0
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 24, 24, 32) 0
_________________________________________________________________
dropout_1 (Dropout) (None, 24, 24, 32) 0
_________________________________________________________________
conv2d_3 (Conv2D) (None, 24, 24, 64) 18496
_________________________________________________________________
activation_3 (Activation) (None, 24, 24, 64) 0
_________________________________________________________________
conv2d_4 (Conv2D) (None, 22, 22, 64) 36928
_________________________________________________________________
activation_4 (Activation) (None, 22, 22, 64) 0
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 11, 11, 64) 0
_________________________________________________________________
dropout_2 (Dropout) (None, 11, 11, 64) 0
_________________________________________________________________
flatten_1 (Flatten) (None, 7744) 0
_________________________________________________________________
dense_1 (Dense) (None, 512) 3965440
_________________________________________________________________
activation_5 (Activation) (None, 512) 0
_________________________________________________________________
dropout_3 (Dropout) (None, 512) 0
_________________________________________________________________
dense_2 (Dense) (None, 37) 18981
_________________________________________________________________
activation_6 (Activation) (None, 37) 0
=================================================================

今回は以下のサイトのモデルを使用してみることにします。
qiita.com
こちらのサイトではCIFAR-10のデータセットの検証制度を90%超えるようなモデルを作成していました。実際にこのモデルをThe Oxford-IIIT Pet Datasetに用いた際にどれだけの精度になるかが気になったということもあります。

モデルのコードは以下のようになりました。

model = Sequential()
model.add(Conv2D(64, (1, 1), input_shape=X_train.shape[1:]))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Conv2D(64, (3, 3)))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Conv2D(64, (5, 5)))
model.add(BatchNormalization())
model.add(Activation('relu'))          
model.add(Dropout(0.25))
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Conv2D(128, (1, 1)))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Conv2D(128, (3, 3)))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Conv2D(128, (5, 5)))
model.add(BatchNormalization())
model.add(Activation('relu'))          
model.add(Dropout(0.25))
          
model.add(Conv2D(256, (1, 1)))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Conv2D(256, (3, 3)))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Conv2D(256, (5, 5)))
model.add(BatchNormalization())
model.add(Activation('relu'))          
model.add(Dropout(0.25))
                    
model.add(Flatten())
model.add(Dense(dense_size)) # 37種類分のカテゴリへ出力
model.add(Activation('softmax'))

またモデル構成は以下のようになります。見ての通り層を増やしました。しかもはCIFAR-10のデータセットの検証制度を90%超えるようなモデルということで期待が高まります!


_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_2 (Conv2D) (None, 64, 64, 64) 256
_________________________________________________________________
batch_normalization_1 (Batch (None, 64, 64, 64) 256
_________________________________________________________________
activation_1 (Activation) (None, 64, 64, 64) 0
_________________________________________________________________
conv2d_3 (Conv2D) (None, 62, 62, 64) 36928
_________________________________________________________________
batch_normalization_2 (Batch (None, 62, 62, 64) 256
_________________________________________________________________
activation_2 (Activation) (None, 62, 62, 64) 0
_________________________________________________________________
conv2d_4 (Conv2D) (None, 58, 58, 64) 102464
_________________________________________________________________
batch_normalization_3 (Batch (None, 58, 58, 64) 256
_________________________________________________________________
activation_3 (Activation) (None, 58, 58, 64) 0
_________________________________________________________________
dropout_1 (Dropout) (None, 58, 58, 64) 0
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 29, 29, 64) 0
_________________________________________________________________
conv2d_5 (Conv2D) (None, 29, 29, 128) 8320
_________________________________________________________________
batch_normalization_4 (Batch (None, 29, 29, 128) 512
_________________________________________________________________
activation_4 (Activation) (None, 29, 29, 128) 0
_________________________________________________________________
conv2d_6 (Conv2D) (None, 27, 27, 128) 147584
_________________________________________________________________
batch_normalization_5 (Batch (None, 27, 27, 128) 512
_________________________________________________________________
activation_5 (Activation) (None, 27, 27, 128) 0
_________________________________________________________________
conv2d_7 (Conv2D) (None, 23, 23, 128) 409728
_________________________________________________________________
batch_normalization_6 (Batch (None, 23, 23, 128) 512
_________________________________________________________________
activation_6 (Activation) (None, 23, 23, 128) 0
_________________________________________________________________
dropout_2 (Dropout) (None, 23, 23, 128) 0
_________________________________________________________________
conv2d_8 (Conv2D) (None, 23, 23, 256) 33024
_________________________________________________________________
batch_normalization_7 (Batch (None, 23, 23, 256) 1024
_________________________________________________________________
activation_7 (Activation) (None, 23, 23, 256) 0
_________________________________________________________________
conv2d_9 (Conv2D) (None, 21, 21, 256) 590080
_________________________________________________________________
batch_normalization_8 (Batch (None, 21, 21, 256) 1024
_________________________________________________________________
activation_8 (Activation) (None, 21, 21, 256) 0
_________________________________________________________________
conv2d_10 (Conv2D) (None, 17, 17, 256) 1638656
_________________________________________________________________
batch_normalization_9 (Batch (None, 17, 17, 256) 1024
_________________________________________________________________
activation_9 (Activation) (None, 17, 17, 256) 0
_________________________________________________________________
dropout_3 (Dropout) (None, 17, 17, 256) 0
_________________________________________________________________
flatten_1 (Flatten) (None, 73984) 0
_________________________________________________________________
dense_1 (Dense) (None, 37) 2737445
_________________________________________________________________
activation_10 (Activation) (None, 37) 0
=================================================================

気になる結果

気になる結果は以下のようになりました。
検証損失: 8.299899394844479
検証精度: 0.14614343643188477

。。。はい、検証制度0.146ですね。。。低いです。
一応、精度と損失の推移プロットを以下に示します。(もう投げやり(2回目))
右が精度のプロット、左が損失のプロットです。

f:id:appare99:20200530103244p:plainf:id:appare99:20200530103318p:plain

CIFAR-10のデータセットの検証制度を90%超えるようなモデルを用いてもThe Oxford-IIIT Pet Datasetでは14.6%の精度にしかならないんですね。。。
というか、下がるとは思ってませんでした。少なくとも上がると思ってました。
これが、モデルの構築は職人技になってしまうと言われる要因なんでしょうかね。

余談(自分なりに作成したモデルについて)
上記のモデルは参考にしたサイトがありましたが、なにも参考にせず、自分なりに最初に作ったモデルのコードが以下となります。

model = Sequential()
model.add(Conv2D(32, (3, 3), padding='same',input_shape=X_train.shape[1:]))
model.add(Activation('relu')) # 活性化関数ReLU

model.add(Conv2D(32, (3, 3), padding='same'))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.2))

model.add(Conv2D(32, (3, 3), padding='same'))
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='same'))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.2))
          
model.add(Conv2D(32, (3, 3), padding='same'))
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='same'))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.2))
          
model.add(Conv2D(32, (3, 3), padding='same'))
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='same'))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.2))

model.add(Conv2D(32, (3, 3), padding='same'))
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='same'))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.2))
          
model.add(Flatten())
model.add(Dense(512)) # 512は出力の次元数
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(dense_size)) # 37種類分のカテゴリへ出力
model.add(Activation('softmax'))

で結果は
検証損失: 3.6147831518691995
検証精度: 0.02435723878443241
2%ってほぼ当てずっぽうですよね。
一応推移も載せときます。
これも上記と同様に右が精度のプロット、左が損失のプロットです。

f:id:appare99:20200530192159p:plainf:id:appare99:20200530192221p:plain
いやぁ、ひどいっすね。。。
層を増やせばある程度精度は上がると思っていましたがそんなことはないのですね。。。
すいません、なぜここまで低い精度になったかはっきりといった原因がわからないのですが、この結果からまだモデルを自作するにはまだハードルが高そうです。。。

改良策③:学習データを増やす

一般的に訓練データ数は多ければ多いほど精度が上がると言われているのでデータを増やします。
正確にはデータを収集することで増やすのではなく、疑似的にデータを増やします。これはデータ拡張、または、Data Augmentationと呼ばれます。
具体例としては

  • 上下左右をずらす
  • 上下左右反転
  • 拡大縮小
  • 回転
  • 一部を切り取る
  • コントラストを変える

などがあります。
さて、前回の記事(上記「はじめに、」段落のリンク参照)でも記載しましたが、今回のData Augmentationは以下のサイトを参考にさせていただきます。
www.kumilog.net

今回は

  • 左右反転
  • 上下反転
  • Cutout という画像の一部マスクする手法を用いる

を用いることにいたします。
それでは一つ一つ見ていきましょう。

左右反転
左右反転は以下のようなプログラムになります。

DATAPATH = "./data/src_augmentation/" 
folder = os.listdir(DATAPATH) 

dense_size  = len(folder)

for index, name in enumerate(folder):    
    files = glob.glob(DATAPATH + name + "/*.jpg") #ディレクトリごとに画像をまとめて格納
    for i, file in enumerate(files): # 1つのディレクトリの画像に対し、一つ一つ画像を見ていく
        image = Image.open(file)
        image = image.convert("RGB") 
        image = np.array(image, dtype=np.float32)
        image = image[:, ::-1, :] # ★画像を左右反転
        image = Image.fromarray(np.uint8(image))
        image_path_separate = file.split('_') # ./data/src_augmentation/abyssinian\Abyssinian_1.jpgの1.jpgを抽出
        image_path_separate2 = image_path_separate[-1].split('.') # 1.jpgの1を抽出
        image.save("./data/output/" + name +  "/" + name + "_" + image_path_separate2[0] + "_flip_horizontal.jpg")

実際の左右反転した画像は以下のようになりました。左が元画像で右が左右反転した画像になります。

f:id:appare99:20200530195048j:plainf:id:appare99:20200530195123j:plain

上下反転
上下反転は以下のようなプログラムになります。

DATAPATH = "./data/src_augmentation/" 
folder = os.listdir(DATAPATH) 

dense_size  = len(folder)

for index, name in enumerate(folder):    
    files = glob.glob(DATAPATH + name + "/*.jpg") #ディレクトリごとに画像をまとめて格納
    for i, file in enumerate(files): # 1つのディレクトリの画像に対し、一つ一つ画像を見ていく
        image = Image.open(file)
        image = image.convert("RGB")
        image = np.array(image, dtype=np.float32)
        image = image[::-1, :, :] # ★画像上下反転
        image = Image.fromarray(np.uint8(image))
        image_path_separate = file.split('_') # ./data/src_augmentation/abyssinian\Abyssinian_1.jpgの1.jpgを抽出
        image_path_separate2 = image_path_separate[-1].split('.')
        image.save("./data/output/" + name + "/" + name + "_" + image_path_separate2[0] + "_flip_upside_down.jpg")

実際の上下反転した画像は以下のようになりました。左が元画像で右が上下反転した画像になります。

f:id:appare99:20200530195048j:plainf:id:appare99:20200530200022j:plain

Cutout
そして最後にCutout。Cutoutは対象画像の一部を単色で塗りつぶす方法です。今回は画像の縦と横の1/3を塗りつぶすようにしています。
コードは以下のようになります。
まずはCutoutの関数部分

def cutout(image_origin, mask_size_h, mask_size_w):
    image = np.copy(image_origin)
    mask_value = image.mean()

    h, w, _ = image.shape
    top = np.random.randint(0 - mask_size_h // 2, h - mask_size_h)
    left = np.random.randint(0 - mask_size_w // 2, w - mask_size_w)
    bottom = top + mask_size_h
    right = left + mask_size_w

    if top < 0:
        top = 0
    if left < 0:
        left = 0

    image[top:bottom, left:right, :].fill(mask_value)
    return image

そして、Cutoutの実装部分(といっても左右反転や上下反転のときと同様な感じで関数代入させただけですが。。。)

DATAPATH = "./data/src_augmentation/" 
folder = os.listdir(DATAPATH) 

dense_size  = len(folder)

for index, name in enumerate(folder):    
    files = glob.glob(DATAPATH + name + "/*.jpg") #ディレクトリごとに画像をまとめて格納
    for i, file in enumerate(files): # 1つのディレクトリの画像に対し、一つ一つ画像を見ていく
        image = Image.open(file)
        image = image.convert("RGB")
        image = np.array(image, dtype=np.float32)
        h, w, _ = image.shape
        mask_size_h = h // 3
        mask_size_w = w // 3
        
        image = cutout(image, mask_size_h, mask_size_w)
        image = Image.fromarray(np.uint8(image))
        image_path_separate = file.split('_')
        image_path_separate2 = image_path_separate[-1].split('.')
        image.save("./data/output/" + name + "/" + name + "_" + image_path_separate2[0] + "_cutout.jpg")

実際のCutoutした画像は以下のようになりました。左が元画像で右がCutoutした画像になります。

f:id:appare99:20200530195048j:plainf:id:appare99:20200530200843j:plain

以上より各カテゴリー画像毎の枚数は約200枚から約800枚に増えました。

気になる結果
結果は以下のようになりました。
検証損失: 2.4042056906279794
検証精度: 0.5453622341156006
おっ!精度が増えている!!
そして推移プロットは次のようになります。
これも上記と同様に右が精度のプロット、左が損失のプロットです。
f:id:appare99:20200530202026p:plainf:id:appare99:20200530202034p:plain

まとめ

長くなりそうなので、今回はこの辺でやめます。
リベンジと言いながら、結局50%ほどの精度にしかなりませんでした。しかし、Data Augmentationの実装、そして、その有効性を肌で感じることができたことは大きな収穫と言えます。

そして、結局、目標としていた精度85%には到達できませんでしたが、希望として学習データを増やせばより精度が上がることが見えました。
今後はデータを増やしてみることで精度を上げてみようと思います。

また、モデルの構築も適切なものを用意すればさらに精度が向上すると思われます。
今後はCNNモデルについて考察及び実装し、精度が向上するかを試してみようと思います。(というかいろんなモデルを勉強できるいい機会になりそうです。)
モデルの自作ですが、なんかとてもハードルが高いような気がしました。モデル自体を自分で作るとなるとかなりの知識が必要になるかもしれません。。。
CNNのモデルは様々なモデルが考案されているのでそこら辺を調べてみようと思います。