学生による学生のためのデータサイエンス勉強会

【ゼロから作るディープラーニング#15】CNN実装まとめ、代表的なCNN【p229-239】

https://bdarc.net/wp-content/uploads/2020/05/icon-1.pnghistoroid

こんにちは。清野です。

今回はCNNの実装のまとめです。

さらに代表的なCNNについても言及します。

CNN実装まとめ、代表的なCNN

今回は教科書229ページからの内容です。

これまで畳み込み、プーリング、全結合層について学んできました。これらを踏まえて、CNNを実装します。

CNNクラスの初期化コードを確認する

SimpleConvNetの確認

class SimpleConvNet:
    """単純なConvNet
    conv - relu - pool - affine - relu - affine - softmax
    
    Parameters
    ----------
    input_size : 入力サイズ(MNISTの場合は784)
    hidden_size_list : 隠れ層のニューロンの数のリスト(e.g. [100, 100, 100])
    output_size : 出力サイズ(MNISTの場合は10)
    activation : 'relu' or 'sigmoid'
    weight_init_std : 重みの標準偏差を指定(e.g. 0.01)
        'relu'または'he'を指定した場合は「Heの初期値」を設定
        'sigmoid'または'xavier'を指定した場合は「Xavierの初期値」を設定
    """
    def __init__(self, input_dim=(1, 28, 28), 
                 conv_param={'filter_num':30, 'filter_size':5, 'pad':0, 'stride':1},
                 hidden_size=100, output_size=10, weight_init_std=0.01):
        filter_num = conv_param['filter_num']
        filter_size = conv_param['filter_size']
        filter_pad = conv_param['pad']
        filter_stride = conv_param['stride']
        input_size = input_dim[1]
        conv_output_size = (input_size - filter_size + 2*filter_pad) / filter_stride + 1
        pool_output_size = int(filter_num * (conv_output_size/2) * (conv_output_size/2))

        # 重みの初期化
        self.params = {}
        self.params['W1'] = weight_init_std * \
                            np.random.randn(filter_num, input_dim[0], filter_size, filter_size)
        self.params['b1'] = np.zeros(filter_num)
        self.params['W2'] = weight_init_std * \
                            np.random.randn(pool_output_size, hidden_size)
        self.params['b2'] = np.zeros(hidden_size)
        self.params['W3'] = weight_init_std * \
                            np.random.randn(hidden_size, output_size)
        self.params['b3'] = np.zeros(output_size)

        # レイヤの生成
        self.layers = OrderedDict()
        self.layers['Conv1'] = Convolution(self.params['W1'], self.params['b1'],
                                           conv_param['stride'], conv_param['pad'])
        self.layers['Relu1'] = Relu()
        self.layers['Pool1'] = Pooling(pool_h=2, pool_w=2, stride=2)
        self.layers['Affine1'] = Affine(self.params['W2'], self.params['b2'])
        self.layers['Relu2'] = Relu()
        self.layers['Affine2'] = Affine(self.params['W3'], self.params['b3'])

        self.last_layer = SoftmaxWithLoss()

ちょっと長いですが、まずは下から見ていきましょう。

最後にレイヤーを作っていますね。このクラスではSimpleConvNetと名付けたネットワークを作ることができますが、そのネットワークの構造は’Conv1′, ‘Relu1’, ‘Pool1′, Affine1′, Relu2’, ‘Affine2’, ‘Softmax’ という順序になっています。

引数

抜き出しながら解説します。

    def __init__(self, input_dim=(1, 28, 28), 
                 conv_param={'filter_num':30, 'filter_size':5, 'pad':0, 'stride':1},
                 hidden_size=100, output_size=10, weight_init_std=0.01):

書いてあるとおりですが、このクラスをオブジェクト化するためには5つの引数が必要になります。

input_dimでは画像の次元をテンソルとして指定します。MNISTの場合、(1, 28, 28)です。1が先にくるので、チャネルファーストですね。

filter_numは、カーネル(フィルタ)の数です。フィルタの枚数が、畳み込みのアウトプットのチャネル数になります。

filter_sizeは、カーネルの大きさを指します。

strideでカーネルを何ピクセルずつずらすかを指定します。

padはパディングするピクセル数です。filter_sizestridepadで1つのカーネルで行える畳み込みの回数が決定します。

hidden_sizeでは、全結合層のニューロン数を指定します。全結合層では特徴マップのピクセルに重みがかけられたあと1列に並びます。その列に何個のピクセルが並ぶのかを指定しているわけですね。

output_sizeでは、出力層のニューロンの数を指定します。これは分類クラスの数と一致していなければなりません。MNISTの場合10個です。

weight_init_stdは、重みの初期値の標準偏差です。初期化自体はnumpyのランダム化で行いますが、そこで標準偏差を指定することができます。

重みの初期化

# 重みの初期化
self.params = {}
self.params['W1'] = weight_init_std * np.random.randn(filter_num, input_dim[0], filter_size, filter_size)
self.params['b1'] = np.zeros(filter_num)
self.params['W2'] = weight_init_std * np.random.randn(pool_output_size, hidden_size)
self.params['b2'] = np.zeros(hidden_size)
self.params['W3'] = weight_init_std * np.random.randn(hidden_size, output_size)
self.params['b3'] = np.zeros(output_size)

長いですが、重みとバイアスを与えているだけですね。

そして各層ごとに適切なテンソルの大きさを指定しつつ、ランダムな数値を生成しています。

あとは各レイヤーを作っているだけです。

順伝播と損失関数

    def predict(self, x):
        for layer in self.layers.values():
            x = layer.forward(x)

        return x

    def loss(self, x, t):
        """損失関数を求める
        引数のxは入力データ、tは教師ラベル
        """
        y = self.predict(x)
        return self.last_layer.forward(y, t)

layers.forward()とあるので、layersのクラス定義を見ないと中身がわかりませんね。

layers.pyには、ReluやSigmoidなどの種類ごとのメソッドが書かれています。

例えばSigmoidメソッドであれば、入力された値をシグモイド関数へと代入します。同じようにAffineメソッドは、入力されたテンソルを1列に変形してから、重みをかけて、バイアスを足すように設定されています。

各レイヤーをfor文で回すことで、forwardメソッドが順に実行されていきます。その結果、最終的にはクラスの推論が行われます。

backwardメソッドも同じように書かれています。これで損失関数を逆伝播させていくわけですね。

重みの可視化

重みはカーネル(行列)の要素です。実際には数値ですが、数値は色(明るさ)に変換できるため、カーネルを画像としてみることができます。

# coding: utf-8
import numpy as np
import matplotlib.pyplot as plt
from simple_convnet import SimpleConvNet

def filter_show(filters, nx=8, margin=3, scale=10):
    """
    c.f. https://gist.github.com/aidiary/07d530d5e08011832b12#file-draw_weight-py
    """
    FN, C, FH, FW = filters.shape
    ny = int(np.ceil(FN / nx))

    fig = plt.figure()
    fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.05, wspace=0.05)

    for i in range(FN):
        ax = fig.add_subplot(ny, nx, i+1, xticks=[], yticks=[])
        ax.imshow(filters[i, 0], cmap=plt.cm.gray_r, interpolation='nearest')
    plt.show()


network = SimpleConvNet()
# ランダム初期化後の重み
filter_show(network.params['W1'])

# 学習後の重み
network.load_params("params.pkl")
filter_show(network.params['W1'])

初期値の重みからできるカーネルは以下のようなものです。

学習を経て、このパターンが変化していくわけですね。

代表的なCNN

では代表的なCNNを見てみましょう。

LeNet

1998年に発表されたCNNです。

CNNの元祖ともいえるネットワークです。プーリング層として、サブサンプリングを行っています。これはドロップアウトに近いもので、ウィンドウの一部だけがつぎのレイヤーへ入力されます。

また活性化関数もシグモイド関数である点が特徴的です。

AlexNet

第3次AIブームの立役者ですね。

活性化関数にReLUを使うようになり、MaxPooling層で実装されています。さらにDropout層も含まれるようになりました。

2012年に発表されました。当時はGPUも非力だったらしいです。


https://bdarc.net/wp-content/uploads/2020/05/icon-1.pnghistoroid

以上です。簡単なまとめのみですが、また不明なところは教科書を振り替えてみましょう。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です