zuminote

個人的な勉強記録

PythonとKerasによるディープラーニング 第5章

いよいよCNNによるおなじみの画像分類である。

学び: - 原則としてCNNでは、情報を漏れなく拾ったうえで寄与が大きい特徴を抽出したいので、「ストライドなしの畳み込み」→「最大プーリング」を使ったほうがよい。 - Rescaling層の存在

github.com

CNN

まず、2章のMNIST分類をCNNで試してみる。

ちなみに2章のネットワークは以下

from tensorflow import keras
from tensorflow.keras import layers
model = keras.Sequential([
    layers.Dense(512, activation="relu"), #ユニット数512
    layers.Dense(10, activation="softmax") #ユニット数10
])

今回のCNN。

from tensorflow import keras
from tensorflow.keras import layers

inputs = keras.Input(shape=(28, 28, 1))
x = layers.Conv2D(filters=32, kernel_size=3, activation="relu")(inputs)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=64, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=128, kernel_size=3, activation="relu")(x)
x = layers.Flatten()(x)
outputs = layers.Dense(10, activation="softmax")(x)
model = keras.Model(inputs=inputs, outputs=outputs)
>>> model.summary()

Model: "model"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         [(None, 28, 28, 1)]       0         
_________________________________________________________________
conv2d (Conv2D)              (None, 26, 26, 32)        320       
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 13, 13, 32)        0         
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 11, 11, 64)        18496     
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 5, 5, 64)          0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 3, 3, 128)         73856     
_________________________________________________________________
flatten (Flatten)            (None, 1152)              0         
_________________________________________________________________
dense (Dense)                (None, 10)                11530     
=================================================================
Total params: 104,202
Trainable params: 104,202
Non-trainable params: 0
_________________________________________________________________

CNNの入力形状は、(高さ, 幅, チャンネル数)である。

プーリング層でダウンサンプリングされるので、層が深くなるにしたがって幅・高さは縮小する。

最後にDense層の分類器にかけたいので、その前にFlattenで3次元→1次元に平坦化している。

テストデータを使った精度は2章では約97%だったが、今回は約99%まで向上している。

CNNの利点

全結合層は入力特徴空間から大域的なパターンを学習するが、畳み込み層は局所的なパターンを学習する。

CNNが学習するパターンは移動不変であるため、画像のある場所で学習したパターンは、画像の他の部分でも認識できる。全結合層の場合は、新しい場所ではパターンを新たに学習しなければならない。

また、CNNは、1つ目の層ではエッジなど局所的なパターンを学習し、2層目では1層目で学習したパターンの組み合わせから成るより大きなパターンを学習する、というように、空間階層を学習することができる。

畳み込み層

畳み込み層は画像を、フィルタのパターンが含まれるかどうかを表した「特徴マップ」に変換する。

畳み込みは、次に示す2つの主なパラメータによって定義される。 - フィルタサイズ:入力から抽出されたパッチのサイズ。通常は3×3か5×5 - フィルタ数:畳み込みによって計算されるフィルタの数。これが出力特徴マップの深さ(チャンネル数)となる。

プーリング層

プーリングの意義は以下のとおり。

  • 処理の対象となる特徴マップの係数の数を減らす
    • 特徴マップが大きいままだとネットワークのサイズを大きくしなければならず、係数が多すぎるので過学習しやすくなる。
  • 空間階層を学習する
    • プーリングしないと、フィルタにより着目する範囲が狭すぎて、画像の全体的な特徴をうまく抽出できない(小さなピクセル越しに数字を見分けるような感じになってしまう)

MaxPooling層では、局所パッチのうち最大値を採用する最大値プーリングをしている。

平均値プーリングもあるが、最大値プーリングのほうが、特徴に大きく貢献している要素を拾えるので、うまくいく傾向にある。

一番合理的な方法は、ストライドなしの畳み込み層→最大プーリング。

なぜなら、ストライドなしの畳み込み層では特徴量の密なマップを生成でき、それに対して特徴量の最大活性化を調べることができるから。

ストライドありだと疎なマップになってしまい、平均プーリングだと特徴量の平均プレゼンスを調べることになり、特徴量の有無に関する情報を見逃したり弱めたりすることにつながる。

Dogs vs Cats コンペ

データ読み込み

image_dataset_from_directoryを使うと、ディレクトリから直接データセットを生成できる。

from tensorflow.keras.preprocessing import image_dataset_from_directory

train_dataset = image_dataset_from_directory(
    new_base_dir / "train",
    image_size=(180, 180),
    batch_size=32)

ネットワーク

スケーリングをやってくれる層があることを初めて知った。

MNISTよりも大きな画像、複雑な課題を扱うので、ネットワークのキャパシティを増強する。

MNISTでは畳み込み3層、プーリング2層にしていたが、今回は畳み込み5層、プーリング4層。

層を増やすことで、Flatten層に到達したときに特徴マップが大きくなりすぎないようにサイズを削減できる。Flatten層には7×7のサイズで入力される。

CNNでは基本的に、層が深くなるほど、特徴マップの深さが増加・サイズは減少する。

二値分類なので、最終層のユニット数は1,活性化関数にシグモイドを使っている。

二値分類なので当然、損失関数はbinary_crossentropyである。

また、データ拡張部分をモデルの入力部に組み込むように記述できることを知った。

データ拡張により、各エポックで同じ画像でも微妙に変わるので、見かけのデータ数が多くなる。

data_augmentation = keras.Sequential(
    [
        layers.experimental.preprocessing.RandomFlip("horizontal"),
        layers.experimental.preprocessing.RandomRotation(0.1),
        layers.experimental.preprocessing.RandomZoom(0.2),
    ]
)

inputs = keras.Input(shape=(180, 180, 3))
x = data_augmentation(inputs) #データ拡張
x = layers.experimental.preprocessing.Rescaling(1./255)(inputs)  # 0〜1の値にスケーリング
x = layers.Conv2D(filters=32, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=64, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=128, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=256, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=256, kernel_size=3, activation="relu")(x)
x = layers.Flatten()(x)
x = layers.Dropout(0.5)(x)  # ドロップアウト
outputs = layers.Dense(1, activation="sigmoid")(x)

model = keras.Model(inputs=inputs, outputs=outputs)
model.compile(loss="binary_crossentropy",
              optimizer="rmsprop",
              metrics=["accuracy"])

学習済みのCNNを使用する

PyTorchの方でやったような転移学習はもちろんkerasでも可能。

ここでも重要なのは、学習済みモデルの重みを凍結すること。

本書では説明のためか、以下の2パターンの方法を書いていたが、方法1はあまり意味がないので、方法2だけでよいと思う。 - 方法1:まず、新しいデータセットをつかって学習済みモデルconv_baseにpredictさせ、その出力をNumPy配列に格納する。そして、そのNumPy配列を別途生成した全結合分類器(つまり、出力部分)の入力として使用する - 方法2:学習済みモデルconv_baseの重みを凍結させて、入力側にデータ拡張を追加、出力部分を二値分類用につけかえる

以下が方法2のコード。conv_base.trainable = False で学習済みモデル部分の重みを凍結させている。

conv_base  = keras.applications.vgg16.VGG16(
    weights="imagenet",
    include_top=False)
conv_base.trainable = False

data_augmentation = keras.Sequential(
    [
        layers.experimental.preprocessing.RandomFlip("horizontal"),
        layers.experimental.preprocessing.RandomRotation(0.1),
        layers.experimental.preprocessing.RandomZoom(0.2),
    ]
)

inputs = keras.Input(shape=(180, 180, 3))
x = data_augmentation(inputs)
x = keras.applications.vgg16.preprocess_input(x)
x = conv_base(x)
x = layers.Flatten()(x)
x = layers.Dense(256)(x)
x = layers.Dropout(0.5)(x)
outputs = layers.Dense(1, activation="sigmoid")(x)

model = keras.Model(inputs, outputs)
model.compile(loss="binary_crossentropy",
              optimizer="rmsprop",
              metrics=["accuracy"])

テストデータで約97%という高い精度が出た。転移学習+データ拡張が功を奏したといえる。

ファインチューニング

もちろん、学習済みモデルの一部を解凍した状態で学習させることも可能。

例えば、以下のように最後4層だけ学習可能にさせる。

# 最後4層だけ学習可能にする
conv_base.trainable = True

for layer in conv_base.layers[:-4]:
    layer.trainable = False

セグメンテーション

また、サンプルコードのみだが、The Oxford-IIIT Pet Datasetのセグメンテーションの例が載っていた。

データ内容は、画像と、画像と同じ形状のカテゴリラベル(1,2,3など)の配列。

f:id:zuminote:20210812233702p:plainf:id:zuminote:20210812233712p:plain

load_imgでパスから画像ファイルを読み込み、img_to_arrayで配列化し、その配列をデータセットとしてさらにndarrayに格納する、という手順でデータを読み込んでいる。

import numpy as np
import random
img_size = (200, 200)
num_imgs = len(input_img_paths)
random.Random(1337).shuffle(input_img_paths)
random.Random(1337).shuffle(target_paths)

def path_to_input_image(path):
    return img_to_array(load_img(path, target_size=img_size))

def path_to_target(path):
    img = img_to_array(
        load_img(path, target_size=img_size, color_mode="grayscale"))
    img = img.astype("uint8") - 1
    return img

# 画像を読み込んでndarrayに格納
input_imgs = np.zeros((num_imgs,) + img_size + (3,), dtype="float32")
targets = np.zeros((num_imgs,) + img_size + (1,), dtype="uint8")
for i in range(num_imgs):
    input_imgs[i] = path_to_input_image(input_img_paths[i])
    targets[i] = path_to_target(target_paths[i])

モデルは以下。後半の転置畳み込みが、セグメンテーションタスクにおける肝らしい。

参考:

転置畳み込みの謎を解き明かす

ニューラルネットワークにおけるDeconvolution - Qiita

KerasのConv2DTransposeの動作について - Qiita

def get_model(img_size, num_classes):
    inputs = keras.Input(shape=img_size + (3,))
    x = layers.experimental.preprocessing.Rescaling(1./255)(inputs)
    x = layers.Conv2D(64, 3, strides=2, activation="relu", padding="same")(x)
    x = layers.Conv2D(64, 3, activation="relu", padding="same")(x)
    x = layers.Conv2D(128, 3, strides=2, activation="relu", padding="same")(x)
    x = layers.Conv2D(128, 3, activation="relu", padding="same")(x)
    x = layers.Conv2D(256, 3, strides=2, padding="same", activation="relu")(x)
    x = layers.Conv2D(256, 3, activation="relu", padding="same")(x)
    x = layers.Conv2DTranspose(256, 3, activation="relu", padding="same")(x)
    x = layers.Conv2DTranspose(256, 3, activation="relu", padding="same", strides=2)(x)
    x = layers.Conv2DTranspose(128, 3, activation="relu", padding="same")(x)
    x = layers.Conv2DTranspose(128, 3, activation="relu", padding="same", strides=2)(x)
    x = layers.Conv2DTranspose(64, 3, activation="relu", padding="same")(x)
    x = layers.Conv2DTranspose(64, 3, activation="relu", padding="same", strides=2)(x)
    outputs = layers.Conv2D(num_classes, 3, activation="softmax", padding="same")(x)
    model = keras.Model(inputs, outputs)
    return model

model = get_model(img_size=img_size, num_classes=3)

# 1,2,3ラベルはone-hotではないので、sparseを使う
model.compile(optimizer="rmsprop", loss="sparse_categorical_crossentropy")
callbacks = [
    keras.callbacks.ModelCheckpoint("oxford_segmentation.keras",
                                    save_best_only=True)
]
history = model.fit(train_input_imgs, train_targets,
                    epochs=50,
                    callbacks=callbacks,
                    batch_size=64,
                    validation_data=(val_input_imgs, val_targets))