2022.02.24

畳み込みニューラルネットワーク (Convolution Neural Networks)のアーキテクチャ及び応用

はじめに

CNN アーキテクチャ : VGG, Resnet, InceptionNet, XceptionNet

使用例:画像特徴抽出+転移学習

画像認識研究のための研究用標準データセットでは、ImageNetデータセットが標準となっております。このデータセットは、22,000以上の日常的なカテゴリを含む、約14,000のハンドラベル付きの注釈付き画像から構成されています。毎年、ImageNetコンテストが開催され、このデータセットの縮小版(1000のカテゴリを含む)を使用して、画像を正確に分類することを目的としています。ImageNet Challengeで優勝した多くのソリューションは、最新の畳み込みニューラルネットワークアーキテクチャを使用して、可能な限り最高の精度のしきい値を達成しています。このカーネルでは、VGG16、19、ResNet、AlexNetなど、これらの一般的なアーキテクチャについて説明します。最後に、事前学習されたモデルを使って画像の特徴を生成して、機械学習モデルに使用する方法を説明します。

3つの主要コンポーネントについて説明します。

CNN アーキテクチャ

 1.1 VGG16

 1.2 VGG19

 1.3 InceptionNet

 1.4 Resnet

 1.5 XceptionNet

2. 画像特徴抽出

3. 転移学習

CNN アーキテクチャ

VGG16

VGG16は2014年に公開されたもので、(Imagenetのコンペティションで使用された他のNNアーキテクチャの中で)最もシンプルなものの一つです。その主な特徴は以下の通りです。

・このネットワークには,重みとバイアスのパラメータを学習するレイヤは16層が含まれています。

・13層の畳み込み層と3層の密な層を重ねて画像分類を目的とします。

・畳み込み層フィルター数は増加するパターンに従います

(オートエンコーダーのデコーダー・アーキテクチャに似ている)。

・情報量の多い特徴は、アーキテクチャの異なるステップで適用されるマックスプーリング層によって得られます。

・密な層は、それぞれ4096、4096、1000のノードで構成されています。

・このアーキテクチャの短所は、学習に時間がかかることと、非常に大きなサイズのモデルを生成することです。

VGG16 アーキテクチャは以下のようになります:

実装例:VGG16

それでは、pythonのkerasライブラリを使って、このアーキテクチャを作成する方法を見てみましょう。以下のコードブロックは、kerasでのVGG16の実装を示しています。

In [1]:
from keras.layers import Input, Conv2D, MaxPooling2D
from keras.layers import Dense, Flatten
from keras.models import Model

_input = Input((224,224,1)) 

conv1  = Conv2D(filters=64, kernel_size=(3,3), padding="same", activation="relu")(_input)
conv2  = Conv2D(filters=64, kernel_size=(3,3), padding="same", activation="relu")(conv1)
pool1  = MaxPooling2D((2, 2))(conv2)

conv3  = Conv2D(filters=128, kernel_size=(3,3), padding="same", activation="relu")(pool1)
conv4  = Conv2D(filters=128, kernel_size=(3,3), padding="same", activation="relu")(conv3)
pool2  = MaxPooling2D((2, 2))(conv4)

conv5  = Conv2D(filters=256, kernel_size=(3,3), padding="same", activation="relu")(pool2)
conv6  = Conv2D(filters=256, kernel_size=(3,3), padding="same", activation="relu")(conv5)
conv7  = Conv2D(filters=256, kernel_size=(3,3), padding="same", activation="relu")(conv6)
pool3  = MaxPooling2D((2, 2))(conv7)

conv8  = Conv2D(filters=512, kernel_size=(3,3), padding="same", activation="relu")(pool3)
conv9  = Conv2D(filters=512, kernel_size=(3,3), padding="same", activation="relu")(conv8)
conv10 = Conv2D(filters=512, kernel_size=(3,3), padding="same", activation="relu")(conv9)
pool4  = MaxPooling2D((2, 2))(conv10)

conv11 = Conv2D(filters=512, kernel_size=(3,3), padding="same", activation="relu")(pool4)
conv12 = Conv2D(filters=512, kernel_size=(3,3), padding="same", activation="relu")(conv11)
conv13 = Conv2D(filters=512, kernel_size=(3,3), padding="same", activation="relu")(conv12)
pool5  = MaxPooling2D((2, 2))(conv13)

flat   = Flatten()(pool5)
dense1 = Dense(4096, activation="relu")(flat)
dense2 = Dense(4096, activation="relu")(dense1)
output = Dense(1000, activation="softmax")(dense2)

vgg16_model  = Model(inputs=_input, outputs=output)

学習済みモデル : VGG16

Kerasライブラリは、事前に学習されたモデルを提供しており、保存されたモデルの重みをロードして、転移学習、画像特徴抽出、物体検出などの異なる目的に使用することができます。ライブラリで与えられたモデルアーキテクチャをロードし、すべての重みをそれぞれの層に追加します。

学習済みモデルを使用する前に、予測を行うための関数をいくつか書いてみましょう。まず、画像を読み込んで前処理を行います。

In [2]:
from keras.applications.vgg16 import decode_predictions
from keras.applications.vgg16 import preprocess_input
from keras.preprocessing import image
import matplotlib.pyplot as plt 
from PIL import Image 
import seaborn as sns
import pandas as pd 
import numpy as np 
import os 

img1 = "./datasets/dogs-vs-cats-redux-kernels-edition/train/cat.11679.jpg"
img2 = "./datasets/dogs-vs-cats-redux-kernels-edition/train/dog.2811.jpg"
img3 = "./datasets/flowers/sunflower/7791014076_07a897cb85_n.jpg"
img4 = "./datasets/fruits-360_dataset/fruits-360/Training/Banana/254_100.jpg"
imgs = [img1, img2, img3, img4]

def _load_image(img_path, img_size):
    img = image.load_img(img_path, target_size=img_size)
    img = image.img_to_array(img)
    img = np.expand_dims(img, axis=0)
    img = preprocess_input(img)
    return img 

def _get_predictions(model, img_size):
    f, ax = plt.subplots(1, 4)
    f.set_size_inches(80, 40)
    for i in range(4):
        ax[i].imshow(Image.open(imgs[i]).resize((200, 200), Image.ANTIALIAS))
    plt.show()
    
    f, axes = plt.subplots(1, 4)
    f.set_size_inches(80, 20)
    for i,img_path in enumerate(imgs):
        img = _load_image(img_path, img_size)
        preds  = decode_predictions(model.predict(img), top=3)[0]
        b = sns.barplot(y=[c[1] for c in preds], x=[c[2] for c in preds], color="gray", ax=axes[i])
        b.tick_params(labelsize=55)
        f.tight_layout()

ここで、以下の手順を実行してみましょう。

・keras.applicationsからVGG16アーキテクチャをインポートする。

・保存した重みをアーキテクチャに追加する。

・モデルを使って予測を行う。

In [3]:
from keras.applications.vgg16 import VGG16
vgg16_weights = './weights/vgg16/vgg16_weights_tf_dim_ordering_tf_kernels.h5'
vgg16_model = VGG16(weights=vgg16_weights)
_get_predictions(vgg16_model, img_size=(224, 224))

VGG19

VGG19は、VGG16と同様のモデル・アーキテクチャーに3つの畳み込み層を追加したもので、合計16の畳み込み層と3つの密な層で構成されています。 以下に、VGG19モデルのアーキテクチャを示します。VGGネットワークでは、ストライド1の3×3畳み込みを使用することで、7×7に相当する有効受容フィルターが得られます。これは、学習するパラメータが少ないことを意味します。

In [4]:
from keras.applications.vgg19 import VGG19
vgg19_weights = './weights/vgg19/vgg19_weights_tf_dim_ordering_tf_kernels.h5'
vgg19_model = VGG19(weights=vgg19_weights)
_get_predictions(vgg19_model, img_size=(224, 224))

InceptionNets

GoogleNetとも呼ばれるこのモデルは、全22層で構成されており、2014 ImageNet Challengeの優勝モデルです。

・インセプション・モジュールは、InceptionNetsの基本単位構造(ブロック)です。インセプション・モジュールの重要なアイデアは、優れた ローカル・ネットワーク・トポロジー(ネットワーク内のネットワーク)を設計することです。

・これらのモジュールまたはブロックは、マルチレベルの特徴抽出器として機能し、多様な特徴マップを作成するために、異なるサイズの畳み込みを行います。

・インセプション・モジュールは、1×1畳み込みブロックで構成されており、その役割は次元削減を行うことです。

・1x1の畳み込みを行うことで、インセプション・ブロックは空間的な次元を維持しつつ、奥行きを縮小します。そのため、ネットワーク全体の次元が指数関数的に増加することはありません。

・このネットワークは、通常の出力層とは別に、2つの補助的な分類出力で構成されており、これは下位の層でグラデーションを注入するために使用されます。

インセプション・モジュールは次の図のようになっています。

アーキテクチャの全体像を以下に示します。

学習済みモデル : InceptionV3

In [5]:
from keras.applications.inception_v3 import InceptionV3
inception_weights = './weights/inceptionv3/inception_v3_weights_tf_dim_ordering_tf_kernels.h5'
inception_model = InceptionV3(weights=inception_weights)
_get_predictions(inception_model, img_size=(299, 299))

Resnets

参考文献:

https://arxiv.org/pdf/1512.03385.pdf

これまでのモデルでは、深層ニューラルネットワークを使用しており、畳み込み層を次々と重ねていました。これまでのモデルでは、深いニューラルネットワークを使用していましたが、ディープニューラルネットワークの方が性能が高いと言われていました。しかし、実際にはそうではないことがわかりました。深いネットワークには以下のような問題があります。

・ネットワークの最適化が難しくなる

・勾配消失/勾配爆発問題

・劣化問題 (精度が飽和してから劣化する)

Skip Connections

これらの問題を解決するために、resnetアーキテクチャの作者は、深い層も浅い層と同じように何かを学習できるはずだという仮説のもと、Skip Connectionというアイデアを考えました。浅い層の活性化をコピーして、追加の層に同一のマッピングを設定するという解決策が考えられます。このような接続を可能にするのが、次の図に示すスキップ接続である。

これらの接続の役割は、浅い層の活性化に対して同一の関数を実行することであり、結果的に同じ活性化を生み出す。この出力は、次の層の活性化に追加されます。このような接続を可能にしたり、基本的にこの加算動作を可能にするためには、ネットワーク全体で同じサイズの畳み込みを確保する必要がありますが、Resnetが全体で同じ3×3の畳み込みを持つのはそのためです。

主な利点

ネットワークに残差ブロックを使用することで、新しい層が実際に入力データの新しい基本的なパターンを学習するのに役立っているという仮説のもと、任意の深さのネットワークを構築することができます。 この論文の著者は、152層のディープニューラルネットワークアーキテクチャを作成することができました。Resnet34、resnet50、resnet101などのResnetsバリエーションは、Imagenet のコンペで非常に高い精度の解を出しています。

なぜうまくいくのか?

ここでは、なぜ残差ネットワークが成功し、重要な問題を起こさずに、つまりネットワークの性能を落とさずに、層をどんどん追加することができるのかを説明します

図のように、残差ネットワークのない普通のニューラルネットワーク(A)を考えます。ネットワーク(A)では、入力Xがこのニューラルネットワーク(NN)に渡され、活性化A1が与えられます。


次に、より深いネットワーク(B)を考えてみましょう。このネットワークでは、前のネットワークに残差ブロック(2つの追加層とスキップ接続を持つ)が追加されています。ここでは、活性化A1が残差ブロックに渡され、それが新たな活性化A3を与えることになります。

スキップ接続がなかった場合、A3は

A3 = relu ( W2 . A2 + b2) ..... (スキップ接続なし)

ここで、W2とb2は層L2に関連する重みとバイアスです。しかし、スキップ接続では、別の項A1がL2に渡されます。そのため、A3の式は次のように変更されます。

A3 = relu ( W2 . A2 + b2 + A1)

L2正則化や重み減衰法を用いると、W2とb2は強制的にゼロに近い値になる。最悪の場合、これらがゼロになってしまうと

A3 = relu (A1)

なぜなら、reluは負なら0、正ならA1を出力し、A1はreluの以前の活性化で正であることがわかっているからです。

A3 = A1

これは、恒等関数が残差ブロックにとって学習しやすいことを意味します。残差ブロックを追加しても、モデルの複雑さは増しませんでした。なぜなら、これは前の活性化を次の層にコピーしているだけだからです。しかし、これはあくまでも最悪のケースですが、これらの追加層が何か有用なものを学習することが判明するかもしれません。その場合は、ネットワークのパフォーマンスが向上します。

したがって、残差ブロックやスキップ接続を追加しても、ネットワークの性能は低下せず、むしろ新しい層が有用なことを学習する可能性が高まります。

それでは、Resnet50モデルを使って、どのように使用するかを見てみましょう。

In [6]:
from keras.applications.resnet50 import ResNet50

resnet_weights = './weights/resnet50/resnet50_weights_tf_dim_ordering_tf_kernels.h5'
resnet_model = ResNet50(weights=resnet_weights)
_get_predictions(resnet_model, img_size=(224,224))

Xception Nets

Xceptionは、標準のInceptionモジュールを深さ順に分離可能なコンボリューションに置き換えたInceptionアーキテクチャの拡張版です。

In [7]:
from keras.applications.xception import Xception
xception_weights = './weights/xception/xception_weights_tf_dim_ordering_tf_kernels.h5'
xception_model = Xception(weights=xception_weights)

学習済みモデルを使って、画像特徴抽出

ここでは、学習済みモデルを使って特徴を抽出し、抽出した特徴を機械学習に利用する方法を見てみましょう。

最初のステップは、学習済みモデルの重みをモデル・アーキテクチャにロードすることです。追加の引数としてinclude_top = Falseが渡されていることに注目してください。これは、このアーキテクチャの最後のレイヤーを追加したくないことを示しています。

In [8]:
resnet50 = ResNet50(weights='imagenet', include_top=False)

Downloading data from https://storage.googleapis.com/tensorflow/keras-applications/resnet/resnet50_weights_tf_dim_ordering_tf_kernels_notop.h5 94773248/94765736 [==============================] - 3s 0us/step

次のステップとして、このモデルに画像を渡して、特徴を識別していきます。

In [9]:
def _get_features(img_path):
    img = image.load_img(img_path, target_size=(224, 224))
    img_data = image.img_to_array(img)
    img_data = np.expand_dims(img_data, axis=0)
    img_data = preprocess_input(img_data)
    resnet_features = resnet50.predict(img_data)
    return resnet_features

img_path = "./datasets/dogs-vs-cats-redux-kernels-edition/train/dog.2811.jpg"
resnet_features = _get_features(img_path)

抽出された特徴は、変数resnet_featuresに格納されます。これらをMLモデルで使用するために、flattensqueezeすることができます。 flattenは、特徴量の要素の長いベクトルを生成します。squeezeは、特徴量の3次元行列を生成します。

In [10]:
features_representation_1 = resnet_features.flatten()
features_representation_2 = resnet_features.squeeze()

print ("Shape 1: ", features_representation_1.shape)
print ("Shape 2: ", features_representation_2.shape)

Shape 1: (100352,)
Shape 2: (7, 7, 2048)

転移学習

ここでは,学習済みモデルの特徴を用いた転移学習の実装について見てみましょう.まず、バナナとイチゴという2つのクラスの画像を含むデータセットを作成します。また、両方のクラスの画像を集めたテストデータセットも作成します。

データセットの準備

In [11]:
basepath = "./datasets/fruits-360_dataset/fruits-360/Training/"
class1 = os.listdir(basepath + "Banana/")
class2 = os.listdir(basepath + "Strawberry/")

data = {
            'banana': class1[:10], 
            'strawberry': class2[:10], 
            'test': [class1[11], class2[11]]
        }

転移学習は、2つのステップで実施することができます。

Step 1:画像特徴の抽出
Step 2 : 分類器の学習

Step 1 :学習済みモデルを用いた特徴抽出 (resnet50)

画像を反復し、ポイント2で画像特徴抽出に使ったのと同じ関数を呼び出し、これらの特徴のflatten(一元配列)を使用します。

In [12]:
features = {"banana" : [], "strawberry" : [], "test" : []}
testimgs = []
for label, val in data.items():
    for k, each in enumerate(val):        
        if label == "test" and k == 0:
            img_path = basepath + "/Banana/" + each
            testimgs.append(img_path)
        elif label == "test" and k == 1:
            img_path = basepath + "/Strawberry/" + each
            testimgs.append(img_path)
        else: 
            img_path = basepath + label.title() + "/" + each
        feats = _get_features(img_path)
        features[label].append(feats.flatten())        

次に、特徴量を辞書形式からpandasのdataframeに変換します。長いデータフレームが作成されます。このデータフレームには、後で分散フィルタを適用して次元を下げましょう。このステップを避けるための他のアイデア:PCA / SVDを実行して密な特徴を得る。

In [13]:
dataset = pd.DataFrame()
for label, feats in features.items():
    temp_df = pd.DataFrame(feats)
    temp_df['label'] = label
    dataset = dataset.append(temp_df, ignore_index=True)
dataset.head()
Out[13]:
0 1 2 3 4 5 6 7 8 9 ... 100343 100344 100345 100346 100347 100348 100349 100350 100351 label
0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 banana
1 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 banana
2 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 banana
3 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 banana
4 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 banana

5 rows × 100353 columns

データセットからX(予測値)とy(目標値)を用意します。

In [14]:
y = dataset[dataset.label != 'test'].label
X = dataset[dataset.label != 'test'].drop('label', axis=1)

Step 2: 2つのクラスを予測する分類器の作成

ここでは、学習用にsklearnを使って簡単なニューラルネットワーク(多層パーセプトロン分類器)を書きます。

In [15]:
from sklearn.feature_selection import VarianceThreshold
from sklearn.neural_network import MLPClassifier
from sklearn.pipeline import Pipeline

model = MLPClassifier(hidden_layer_sizes=(100, 10))
pipeline = Pipeline([('low_variance_filter', VarianceThreshold()), ('model', model)])
pipeline.fit(X, y)

print ("Model Trained on pre-trained features")

Model Trained on pre-trained features

新しい画像で出力を予測し、結果を確認してみましょう。

In [16]:
preds = pipeline.predict(features['test'])

f, ax = plt.subplots(1, 2)
for i in range(2):
    ax[i].imshow(Image.open(testimgs[i]).resize((200, 200), Image.ANTIALIAS))
    ax[i].text(10, 180, 'Predicted: %s' % preds[i], color='k', backgroundcolor='red', alpha=0.8)
plt.show()

まとめ

以上、上述の通り、たった20行の学習データしかないシンプルなニューラルネットワークでも、テストセットの2つの画像を正しく分類することができるのです。

いかがでしたでしょうか。
弊社クラウドGPUサーバのご利用をご検討中の方はhttps://soroban.highreso.jp/から
お気軽にお申込みやお問い合わせください。

初回インスタンス作成から
3日間は無料利用可能!

新規申し込み