KDOG Notebook

機械学習とか、頭の中を整理するために

Convolutional Neural Networks for MNIST

はじめに

全結合層を用いた多層パーセプトロン(MLP)では生データを一次元ベクトルに変換し、入力データとして処理を実行していた。畳み込みニューラルネットワーク(CNN)では画像の三次元データをそのまま利用することで物体の位置や方向を認識する。脳の視覚野が情報を処理する機構により近いモデルを使うことになる。

今回はMNISTデータを用いてCNNの精度を確認した。

実装

  • im2col (順伝搬においてテンソルをベクトルに変換)
def im2col(input_data, fil_h, fil_w, stride=1, pad=0):
    """
    N, C, H, W   :: Batch size, Channel, Height and Width of input_data
    fil_h, fil_w :: height and width of filter
    """
    N, C, H, W = input_data.shape
    out_h = (H + 2*pad - fil_h) // stride + 1
    out_w = (W + 2*pad - fil_w) // stride + 1
    
    img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant')
    col = np.zeros((N, C, out_h, out_w, fil_h, fil_w))
    
    for y in range(0,out_h):
        y_max = y*stride + fil_h
        for x in range(0,out_w):
            x_max = x*stride + fil_w
            col[:, :, y, x, :, :] = img[:, :, y*stride:y_max, x*stride:x_max]
        
    col = col.transpose(0, 2, 3, 1, 4, 5).reshape(N*out_h*out_w, -1) # -1 = C*fil_h*fil_w
    return col
  • col2im(逆伝搬においてベクトルをテンソルに変換)
def col2im(col, img_shape, fil_h, fil_w, stride=1, pad=0):
    N, C, H, W = img_shape
    out_h = (H + 2*pad - fil_h) // stride + 1
    out_w = (W + 2*pad - fil_w) // stride + 1
    
    col = col.reshape(N, out_h, out_w, C, fil_h, fil_w).transpose(0, 3, 1, 2, 4, 5)
    img = np.zeros((N, C, H + 2*pad + stride - 1, W + 2*pad + stride - 1))
    
    for y in range(0,out_h):
        y_max = y*stride + fil_h
        for x in range(0,out_w):
            x_max = x*stride + fil_w
            img[:, :, y*stride:y_max, x*stride:x_max] += col[:, :, y, x, :, :]
            
    return img[:, :, pad:H+pad, pad:W+pad]
  • 畳み込み層の実装
class Convolution:
    def __init__(self, filter_shape, stride=1, pad=0):
        in_dim = np.prod(filter_shape[1:])
        out_dim = np.prod(filter_shape[2:]) * filter_shape[0]
        self.W = np.random.uniform(low=-np.sqrt(6/(in_dim + out_dim)),
                                   high=np.sqrt(6/(in_dim + out_dim)),
                                   size=filter_shape).astype('float32')
        self.b = np.zeros(filter_shape[0]).astype('float32')
        self.s = stride
        self.p = pad
        
        self.x = None
        self.x_col = None
        self.W_col = None
        
        self.dW = None
        self.db = None
        
    def forward(self,x):
        FC, C, FH, FW = self.W.shape
        N, C, H, W = x.shape
        
        out_h = (H + 2*self.p - FH) // self.s + 1
        out_w = (W + 2*self.p - FW) // self.s + 1
        
        x_col = im2col(x, FH, FW, self.s, self.p)
        W_col = self.W.reshape(FC,-1).T # -1 = C*FH*FW
        
        out = np.dot(x_col, W_col) + self.b
        out = out.reshape(N, out_h, out_w, FC).transpose(0, 3, 1, 2)
        
        self.x = x
        self.x_col = x_col
        self.W_col = W_col
        
        return out
    
    def backward(self, delta):
        FC, C, FH, FW = self.W.shape
        delta = delta.transpose(0, 2, 3, 1).reshape(-1, FC)
        
        self.db = np.sum(delta, axis=0)
        self.dW = np.dot(self.x_col.T, delta)
        self.dW = self.dW.T.reshape(FC, C, FH, FW)
        
        dx = np.dot(delta, self.W_col.T)
        dx = col2im(dx, self.x.shape, FH, FW, self.s, self.p)
        
        return dx
  • Average poolingの実装
class AveragePooling:
    def __init__(self, filter_shape, stride=1, pad=0):
        self.pool_h = filter_shape[0]
        self.pool_w = filter_shape[1]
        self.s = stride
        self.p = pad
        
        self.x = None
        
    def forward(self, x):
        N, C, H, W = x.shape
        
        out_h = (H + 2*self.p - self.pool_h) // self.s + 1
        out_w = (W + 2*self.p - self.pool_w) // self.s + 1
        
        col = im2col(x, self.pool_h, self.pool_w, self.s, self.p)
        col = col.reshape(-1, self.pool_h*self.pool_w)
        
        out = np.mean(col, axis=1, keepdims=True)
        out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)
        
        self.x = x
        self.batch_size = N
        
        return out
    
    def backward(self, delta):
        N, C, H, W = self.x.shape
        
        delta = delta.transpose(0, 2, 3, 1).flatten()
        delta = np.dot(delta[:,None], np.ones(self.pool_h*self.pool_w)[None,:])
        
        dx_col = delta.reshape(-1, self.pool_h*self.pool_w*C) / N
        dx = col2im(dx_col, self.x.shape, self.pool_h, self.pool_w, self.s, self.p)
        
        return dx
  • Max poolingの実装
class MaxPooling:
    def __init__(self, filter_shape, stride=1, pad=0):
        self.pool_h = filter_shape[0]
        self.pool_w = filter_shape[1]
        self.s = stride
        self.p = pad
        
        self.x = None
        
    def forward(self, x):
        N, C, H, W = x.shape
        
        out_h = (H + 2*self.p - self.pool_h) // self.s + 1
        out_w = (W + 2*self.p - self.pool_w) // self.s + 1
        
        col = im2col(x, self.pool_h, self.pool_w, self.s, self.p)
        col = col.reshape(-1, self.pool_h*self.pool_w)
        
        idx = np.argmax(col, axis=1)
        out = np.max(col, axis=1, keepdims=True)
        out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)
        
        self.x = x
        self.idx = idx
        
        return out
    
    def backward(self, delta):
        N, C, H, W = self.x.shape
        
        delta = delta.transpose(0, 2, 3, 1).flatten()
        dmax = np.zeros((delta.size, self.pool_h*self.pool_w))
        dmax[np.arange(delta.size), self.idx] = delta
        
        dx_col = dmax.reshape(-1, self.pool_h*self.pool_w*C) / N
        dx = col2im(dx_col, self.x.shape, self.pool_h, self.pool_w, self.s, self.p)
        
        return dx

結果

今回用いたCNNの構造を以下に示す。プーリング層にはMax poolingまたはAverage poolingを用いた。

Layer Kernel Stride Output
Input 1x28x28
Convolution 16x5x5 1 16x24x24
Batch Norm
ReLU
Pooling 2x2 2 16x12x12
Convolution 32x5x5 1 32x8x8
Batch Norm
ReLU
Pooling 2x2 2 32x4x4
Affine 256
Batch Norm
ReLU
Affine 10
Softmax

確率的勾配降下法を使って20回の学習を実行した。Max poolingおよびAverage poolingを使った場合、F1スコアはそれぞれ0.982および0.986となった。 全結合層だけで構成されたMLPのスコアが0.980だったのでわずかに精度が上がったと言える。

下図に誤分類した場合の結果を示す。

f:id:kdog08:20170718014601p:plain:w600