LoginSignup
36
39

More than 5 years have passed since last update.

【学習メモ】ゼロから作るDeep Learning【7章】

Last updated at Posted at 2017-09-17

ゼロから作るDeep Learning 7章

7章のテーマは畳み込みニューラルネットワーク(Convolutional neural network:CNN)

全体の構造

CNNはこれまで見てきたニューラルネットワークと同じで、レゴブロックのようにレイヤを組み合わせて作ることが可能です。
新たに次が登場
・「Convolutionレイヤ(畳み込み層)」
・「Poolingレイヤ(プーリング層)」

Kobito.JxbIF2.png

Kobito.SXd9XQ.png

一般的なCNNの特徴
・「Convolution - ReLU - (Pooling)」という流れ
・Pooling層は省略されることもある
・出力に近い層では「Affine - ReLU」という組み合わせが用いられる
・最後の出力層は「Affine - Softmax」の組み合わせ

畳み込み層

次の用語が登場
・パティング
・ストライド

またデータは3次元のデータが登場

全結合層の問題点

全結合の問題点とはデータの構造が"無視"されてしまうこと

例えば画像の場合は通常、縦・横・チャンネル方向の3次元形状
この形状には大切な空間情報が含まれている
例えば
・空間的に近いピクセルは似たような値であったり
・RBGの各チャンネルの間にはそれぞれに密接な関係があったり
・距離の離れたピクセル同士はあまり関わりがなかったり
3次元の形状の中にはくみ取るべき本質的なパターンが潜んでいる

全結合層は上記の形状を無視して、すべて同等のニューロン(同じ次元のニューロン)として扱う
一方、畳み込み層は形状を維持する

CNNでは、畳み込み層の入出力データを特徴マップ(feature map)
入力データを入力特徴マップ(feature map)
出力データを出力特徴マップ(feature map)
と言うこともあります。

畳み込み演算

Kobito.uMcQj4.png

「畳み込み演算」
画像処理でいうところ「フィルター処理」に相当
文献によっては「フィルター」という用語は「カーネル」と言う表現されることもある

このフィルターに使用するパラメータが、全結合のニューラルネットワークにおける「重み」に対応します。

演算例

Kobito.Wo1MG2.png

バイアスを加えた演算

Kobito.gNgZ98.png

パディング

パティング:入力データの周囲に固定データ(例えば0)を埋めること

下図は幅1ピクセル0で埋めています
Kobito.joYXx3.png

ストライド

ストライド:フィルターを適用する位置の間隔のこと

Kobito.WoeNb7.png

出力サイズの計算

入力サイズを(H, W)
フィルターサイズを(FH, FW)
出力サイズを(OH, OW)
パディングをP
ストライドをS
とした際の出力サイズは次の通り

OH = \frac{H + 2P - FH}{S} + 1\\
OW = \frac{W + 2P - FW}{S} + 1

ブロックで考える

3次元の畳み込みの演算を分かりやすく直方体のブロックで考えると次のようになります。

Kobito.ITLGJH.png

上記は出力が1枚の特徴マップです。
言い換えれば、チャンネル数が1の特徴マップということ。

次はチャンネル方法にも複数持たせる際の図になります。

Kobito.l5nAG0.png

バイアス項を追加すると次の通りです。
image.png

バッチ処理

N個のデータに対してバッチ処理を行う際にはデータの形状は同じです

image.png

プーリング層

プーリング:縦・横方向の空烏瞰を小さくする演算

下図では2×2の領域を一つの要素に集約するような処理を行って、空間サイズを小さくします。

Kobito.HL2U8Q.png

この例では、2×2のMaxプーリングをスライド2で行った場合の処理です。

Maxプーリング:領域内の最大値を取る計算
また一般的にプーリングのウィンドウサイズとスライドは同じ値にせってします。

Maxプーリング以外にも領域内の平均値を取るAverageプーリングなどがあります。

プーリング層の特徴

・学習するパラメータがない

プーリングは、対象から最大値(もしくは平均値)をとるだけの処理なので学習すべきパラメータは存在しません

・チャンネル数は変化しない

プーリングの演算によって、入力データを出力データのチャンネル数は変化しません。
(OHとOWは変化するがFNは変化しません)

・微小な位置変化に対してロバスト(頑強)

入力データの小さなズレに対して、プーリングは同じような結果を返します。
そのため、入力データの微小なズレに対してロバストです。

Kobito.rOBXno.png

Convolution/Poolingレイヤの実装

4次元配列

# ランダムにデータを生成
x = np.random.rand(10,1,28,28)
x.shape
# (10, 1, 28, 28)

x[0].shape
# (1, 28, 28)
x[1].shape
# (1, 28, 28)

x[0, 0].shape # x[0][0]でもOK
# (28, 28)

im2colによる展開

畳み込みの実装はこれまでの図の通りに行うとfor文の幾重にも組み合わせる必要があります。またNumPyはfor文を使うと処理が遅くなる。

そのためfor文ではなく、im2colと言う関数を使った実装を行う。
im2colはフィルターにとって都合が良いように入力データを展開する関数です。

image.png

なおこの図ではわかりやすさを重視し、フィルター領域が重ならない例をあげました。

・im2colメリットデメリット
メリット:行列計算に帰着させることが可能のため線形代数のライブラリを有効に活用可能
デメリット:通常よりも多くのメモリを消費する

#----------------------------------------------------
# Parameters
#   input_data : (データ数,チャンネル,高さ,横幅)の4次元配列からなる入力データ
#   filter_h : フィルターの高さ
#   filter_w : フィルターの横幅
#   stride : ストライド
#   pad : パディング
# Returns
#   col : 2次元配列 
#----------------------------------------------------
def im2col(input_data, filter_h, filter_w, stride=1, pad=0):

    N, C, H, W = input_data.shape
    out_h = (H + 2*pad - filter_h)//stride + 1
    out_w = (W + 2*pad - filter_w)//stride + 1

    img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant')
    col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))

    for y in range(filter_h):
        y_max = y + stride*out_h
        for x in range(filter_w):
            x_max = x + stride*out_w
            col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]

    col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1)
    return col

im2colを使って見る

import sys, os
sys.path.append(os.pardir)
from common.util import im2col

x1 = np.random.rand(1, 3, 7, 7)
col1 = im2col(x1, 5, 5, stride=1, pad=0)
print(col1.shape)

x2 = np.random.rand(10, 3, 7, 7)
col2 = im2col(x2, 5, 5, stride=1, pad=0)
print(col2.shape)

結果
(9, 75)
(90, 75)

x1がバッチサイズが1で、チャンネル数が3の7×7のデータ
x2がバッチサイズが10で、チャンネル数が3の7×7のデータ

両方とも2次元目の要素数が75になりましたが、これはフィルターの様相数の総和にあたります。(チャンネル3、サイズ5×5)

Convolutionレイヤの実装

im2colによってデータを展開してしまえば、その後は畳み込み層のフィルター(重み)を1列に展開して、2つの行列の内積を計算するだけです。
これは全結合層のAffineレイヤで行ったこととほとんど同じです。
image.png

class Convolution:
    def __init__(self, W, b, stride=1, pad=0):
        self.W = W
        self.b = b
        self.stride = stride
        self.pad = pad

        # 中間データ(backward時に使用)
        self.x = None   
        self.col = None
        self.col_W = None

        # 重み・バイアスパラメータの勾配
        self.dW = None
        self.db = None

    def forward(self, x):
        FN, C, FH, FW = self.W.shape
        N, C, H, W = x.shape
        out_h = 1 + int((H + 2*self.pad - FH) / self.stride)
        out_w = 1 + int((W + 2*self.pad - FW) / self.stride)

        col = im2col(x, FH, FW, self.stride, self.pad)
        #reshape関数に-1を指定すると、多次元配列の辻褄が合うように要素数をまとめてくれる
        col_W = self.W.reshape(FN, -1).T

        out = np.dot(col, col_W) + self.b
        #最後に出力サイズを適切な形状に整形する
        # reshapeは出力サイズを指定の形状を再構成
        # transposeは軸の順番を入れ替えている
        out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)

        self.x = x
        self.col = col
        self.col_W = col_W

        return out

    def backward(self, dout):
        FN, C, FH, FW = self.W.shape
        dout = dout.transpose(0,2,3,1).reshape(-1, FN)

        # 逆行列の計算自体は次の2行でしており、Affineの時と同じ、違うのは行列の次元の辻褄あわせだけ
        self.db = np.sum(dout, axis=0)
        self.dW = np.dot(self.col.T, dout)

        self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)

        dcol = np.dot(dout, self.col_W.T)
        #im2colと逆の処理
        dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)

        return dx

Poolingレイヤの実装

Convolutionレイヤと同じく、im2colを使って入力データを展開して実装する
ただし、プーリングの場合は、チャンネル方向には独立である点が異なる

class Pooling:
    def __init__(self, pool_h, pool_w, stride=1, pad=0):
        self.pool_h = pool_h
        self.pool_w = pool_w
        self.stride = stride
        self.pad = pad

        self.x = None
        self.arg_max = None

    def forward(self, x):
        N, C, H, W = x.shape
        out_h = int(1 + (H - self.pool_h) / self.stride)
        out_w = int(1 + (W - self.pool_w) / self.stride)

        col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
        col = col.reshape(-1, self.pool_h*self.pool_w)

        arg_max = np.argmax(col, axis=1)
        out = np.max(col, axis=1)
        out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)

        self.x = x
        self.arg_max = arg_max

        return out

    def backward(self, dout):
        dout = dout.transpose(0, 2, 3, 1)

        pool_size = self.pool_h * self.pool_w
        dmax = np.zeros((dout.size, pool_size))
        #flattenは構造を1次元配列に入れ直すこと
        dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten()
        dmax = dmax.reshape(dout.shape + (pool_size,)) 

        dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1)
        dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad)

        return dx

CNNの実装

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import pickle
import numpy as np
from collections import OrderedDict
from common.layers import *
from common.gradient import numerical_gradient

# 単純なConvNet
# conv - relu - pool - affine - relu - affine - softmax
class SimpleConvNet:


    #----------------------------------------------------
    # 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()

    # 推論を行う
    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)

    def accuracy(self, x, t, batch_size=100):
        if t.ndim != 1 : t = np.argmax(t, axis=1)

        acc = 0.0

        for i in range(int(x.shape[0] / batch_size)):
            tx = x[i*batch_size:(i+1)*batch_size]
            tt = t[i*batch_size:(i+1)*batch_size]
            y = self.predict(tx)
            y = np.argmax(y, axis=1)
            acc += np.sum(y == tt) 

        return acc / x.shape[0]

    def numerical_gradient(self, x, t):
        """勾配を求める(数値微分)

        Parameters
        ----------
        x : 入力データ
        t : 教師ラベル

        Returns
        -------
        各層の勾配を持ったディクショナリ変数
            grads['W1']、grads['W2']、...は各層の重み
            grads['b1']、grads['b2']、...は各層のバイアス
        """
        loss_w = lambda w: self.loss(x, t)

        grads = {}
        for idx in (1, 2, 3):
            grads['W' + str(idx)] = numerical_gradient(loss_w, self.params['W' + str(idx)])
            grads['b' + str(idx)] = numerical_gradient(loss_w, self.params['b' + str(idx)])

        return grads

    def gradient(self, x, t):
        """勾配を求める(誤差逆伝搬法)

        Parameters
        ----------
        x : 入力データ
        t : 教師ラベル

        Returns
        -------
        各層の勾配を持ったディクショナリ変数
            grads['W1']、grads['W2']、...は各層の重み
            grads['b1']、grads['b2']、...は各層のバイアス
        """
        # forward
        self.loss(x, t)

        # backward
        dout = 1
        dout = self.last_layer.backward(dout)

        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)

        # 設定
        grads = {}
        grads['W1'], grads['b1'] = self.layers['Conv1'].dW, self.layers['Conv1'].db
        grads['W2'], grads['b2'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
        grads['W3'], grads['b3'] = self.layers['Affine2'].dW, self.layers['Affine2'].db

        return grads

    def save_params(self, file_name="params.pkl"):
        params = {}
        for key, val in self.params.items():
            params[key] = val
        with open(file_name, 'wb') as f:
            pickle.dump(params, f)

    def load_params(self, file_name="params.pkl"):
        with open(file_name, 'rb') as f:
            params = pickle.load(f)
        for key, val in params.items():
            self.params[key] = val

        for i, key in enumerate(['Conv1', 'Affine1', 'Affine2']):
            self.layers[key].W = self.params['W' + str(i+1)]
            self.layers[key].b = self.params['b' + str(i+1)]

要はレイヤーを増やして、隠れ層で使うハイパーパラメータの値を増やしただけで実装可能ということ

学習を実行
また私のMacbook AirだとCPU使用率がすごかったのでデータの削減のコメントアウトを外して実行しました

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from simple_convnet import SimpleConvNet
from common.trainer import Trainer

# データの読み込み
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=False)

# 処理に時間のかかる場合はデータを削減 
#x_train, t_train = x_train[:5000], t_train[:5000]
#x_test, t_test = x_test[:1000], t_test[:1000]

max_epochs = 20

network = SimpleConvNet(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)

trainer = Trainer(network, x_train, t_train, x_test, t_test,
                  epochs=max_epochs, mini_batch_size=100,
                  optimizer='Adam', optimizer_param={'lr': 0.001},
                  evaluate_sample_num_per_epoch=1000)
trainer.train()

# パラメータの保存
network.save_params("params.pkl")
print("Saved Network Parameters!")

# グラフの描画
markers = {'train': 'o', 'test': 's'}
x = np.arange(max_epochs)
plt.plot(x, trainer.train_acc_list, marker='o', label='train', markevery=2)
plt.plot(x, trainer.test_acc_list, marker='s', label='test', markevery=2)
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()

……
train loss:0.0145554384445
train loss:0.0275851756417
train loss:0.00785021651885
train loss:0.00986611950473
=============== Final Test Accuracy ===============
test acc:0.956
Saved Network Parameters!

image.png

CNNの可視化

1層目の重みの可視化

学習前:フィルターがランダムに初期化されているため白黒の濃淡に規則性がない
image.png

学習後:規則性がある
image.png

このような規則性があるフィルターは"何を見ている"のか
エッジ:色が変化する境目
ブロブ:局所的に塊のある領域

階層構造に夜情報抽出

1層目の畳み込み:エッジやブロブなどの低レベルの情報が抽出
畳み込み層を何層も重ねる:より複雑で抽象化された情報が抽出

下記のDEMO 1が引用されていましたhttp://vision03.csail.mit.edu/cnn_art/index.html#v_single

デモでは次の通りでした。
Cov1:エッジ、ブロブ(Edge+Blob)
Cov3:テキスチャ(Texture)
Cov5:物体のパーツ(Object Parts)
Fc8:犬や猫などの物体のクラス(Object Classes)

従って、層が深くなるに連れて、ニューロンは単純な形状から"高度"な情報へと変化していきます。
言い換えれば、モノの「意味」を理解するように、反応する対象が変化していく日です。

代表的なCNN

本書では次を説明
・1998年に初めて提案されたCNNも元祖LeNet
・ディープラーニングが注目を集めるに至った2012年のAlexNet

LeNet

「現在のCNN」と比較すると次の点が異なる
・活性化関数にシグモイド関数を使用
(現在はReLU関数)
・サブサンプリングによって中間データのサイズ縮小を行っている
(現在はMaxプーリング)
http://dx.doi.org/10.1109/5.726791

AlexNet

AlexNetは畳み込み層とプーリング層を重ねて、最後に全結合層を経由して結果を出力します。
LeNetとの以下の点が異なる
・活性化関数にReLU関数を用いる
・LRN(Local Response Normalization)と言う局所的正規化を行う層を用いる
・Dropoutを使用する

今と昔

ネットワーク構成にはLeNet、AlexNeには大きな違いはありませんが、コンピュータ技術に大きな進歩があった
具体的には
・大量のデータを誰でも入手できるようになった
・大量の並列計算を得意とするGPUが普及し、大量の演算を高速に行うことが可能になった

36
39
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
39