본문 바로가기
Deep Learning

별거없는 Inception V1 구현 (Keras, Draft)

by Nev Fiasco 2019. 7. 27.

'별거 없는' 시리즈는, 논문을 리뷰하고 코드로 구현해 보면서, 정말 논문과 코드 둘다 별거 없다는 것을 보여주기 위한 시리즈입니다. 사실상 대학들에서 나오는 논문들을 보면 쓸모없는 것 90%, 쓸모있는 것 10%라고 생각합니다. (물론 모든 논문은 그 나름의 가치가 있습니다만, 거칠게 말해서 그렇다는 것입니다.) 이 시리즈에서는 쓸모있는 것 10%만 뽑아서, 최대한 쉽게 설명하고, 쉽게 쉽게 넘어가려 합니다. 특정 부분에 있어서는 쉽게 얘기하기 위해서, 논문의 쓸데없는 부분을 무시할 수 있으니 참고 바랍니다.


 

들어가면서

2014년 논문이다. 그 당시 ImageNet의 INLSVRC 2014에서 Classification과 Detection분야에서 우승을 한 Architecture이다. 이 이후로 V2, V3 등등이 나왔는데, 일단은 V1만 이해하면 다른 것들은 쉽게 이해할 수 있을 것 같다. 보통은 Classification만을 구현해 보는데, 우리는 한번 Detection까지 어떻게 가는지 알아보도록 하자.


이 논문의 기여 사항

1) 리소스/속도 이슈 풀기 : 일단 기본적으로 Deep Learning은 리소스가 많이 사용되고, 속도가 느린 이슈가 있다. 이것을 해결하기 위한 적절한 Depth와 Width를 가진 Architecture를 Inception V1에서 제안한다. (Hebbian Principle을 사용한다.)

 

  • AlexNet(2년전 것)대비하여 12배 적은 파라미터를 사용했다. 그리고 더 정확하고
  • 2개의 CNN Layer가 연결되어있을때, Filter의 개수가 늘어나면, 그것의 x^2(quadratic)하게 computational power가 증가한다.
  • 대부분의 Weight가 0으로 되었을 때 에는 Filter가 늘더라도 비효율적이다. 단지 Computational Power만 낭비된다.
  • Google팀은 Arora의 논문(Provable bounds for learning some deep representations) 참고했는데, 이는, 생물학적인 모방결과 dataset의 확률적 분포가 크고 아주 sparse한 deep neural network로 표현될 수 있다면, 최적화된 network 구조가 만들어질 수 있다는 것 이다. (마지막 layer의 activation의 관계성 통계를 Analyze해보고, 높은 관련성을 지닌 출력값과 Neurons들과 Clustering을 해본 결과)
    • 위의 Arora논문은 수학적으로 명확한 증명이 필요하나, 단순히 말해 Neuron들은 함께 Fire되고 함께 Wire된다는 Hebbian Principle로써 명확화 될 수 있다.
    • 그런데 Sparse한 Model에 있어서, 현재의 Numerical Computation은 비효율 적이다. (Dense한 모델에 효율적으로 만들어져 있다.) 왜냐하면 Lookup과 Cache misses에 대한 Overhead가 존재(Computer Model이 그렇기 때문) 따라서 Dense Algorithm을 쓸 수 밖에 없다. 그리고 현재 Conv는 Dense한 Connection이다.
    • 이렇게 Sparse(이론적 필요)와 Dense(물리적 필요) 사이에서 우리는 무슨 희망을 찾을 수 있을까? 이를 해결하기 위해 Google팀은 아키텍쳐를 큼직한 모듈로써 Sparse하도록 나누고, 반면 모듈 내부는 Dense하게 계산하도록 하였다.
    • 아래의 그림을 보면 위의 것이 Densely Connected Network이고, 아래의 것이 Sparsely Connected Network이다.

Densely Connected Network

Sparsely Connected Network

  • NiN에서의 Network안에서의 Network의 특징을 가져옴 (Lin et al)
    • 공통적으로 1x1 Conv를 쓴다.
    • 차이점으로 NiN은 Network의 표현력을 높이기 위해서였으나, Inception V1에서는 Dimensional Reduction의 목적이 크다.(Computational Bottleneck을 없애려고).  그리고 Network의 Depth만 증가시키는 것이 아니라 Performacne 이슈없이 Width도 키울 수 있다. (보통 Width가 커지만 Performance가 이슈가 발생됨)

 

2) Overfitting문제 풀기: 기존의 크고 Deep한 모델은, 크면 클수록, 깊으면 깊을 수록 Overfitting하는 경향이 컸다. 특히나 Training Data가 적을때 그러한 경향이 컨다. 그리고 역시나 1)처럼 속도의 이슈를 불러 일으킨다.

 

 


Architecture 특징

특징 설명
Inference 속도 1.5 billion multiply-adds
Layer 개수 27개 Layers
   

 

 


Architecture

1) Overall

Name Patch Size / Stride output size Params OPS Description
Convolution 7 x 7 / 2 112 x 112 x 64 2.7K 34M  
Max Pooling 3 x 3 / 2 56 x 56 x 64      
Convolution 3 x 3 / 1
56 x 56 x 192
112K
360M
 
Max Pooling 3 x 3 /2 28 x 28 x 192      
Inception (3a)   28 x 28 x 256 159K 128M  
Inception (3b)   28 x 28 x 480
380K
304M
 
Max Pooling 3 x 3 / 2 14 x 14 x 480      
Inception (4a)   14 x 14 x 512
364K
73M
 
Inception (4b)   14 x 14 x 512 437K
88M
 
Inception (4c)   14 x 14 x 512 463K
100M
 
Inception (4d)   14 x 14 x 528 580K
119M
 
Inception (4e)   14 x 14 x 832
840K
170M
 
Max Pooling 3 x 3 / 2 7 x 7 x 832      
Inception (5a)
  7 x 7 x 832 1027K
54M
 
Inception (5b)
  7 x 7 x 1024
1388K
71M
 
Average Pooling 7 x 7 / 1 1 x 1 x 1024     즉, Global Average Pooling임. 전체 영역에 대한 Pooling. Output은 결국 Feature의 개수만큼이다.
Dropout (40%)   1 x 1 x 1024      
Linear   1 x 1 x 1000 1000K 1M  
Softmax   1 x 1 x 1000      

 

2) Details

Inception Module (Naive Version)

  • Architecture에서 가장 중요한건, Inception Module이다.
  • 위의 그림은 전체 ARchitecture에서 Inception Module을 설명하고 있다. (위의 것은 Naive버전이다)
  • Inception Layer는 1x1 Conv, 3x3 Conv, 5x5 Conv Layer들의 Combination이다. 그리고 Concatenate Layer에서 Concatenation을 한 후에(single Output Vector로 만들어서) 다음 단계의 Input으로 활용한다.
  • 위의 Layer에 추가적으로 아래와같은 Layer를 만들 수 있다.

Inception Module (Modified)

  • 이를 통해서 Depth와 Width를 늘리면서도, Overfitting을 막고, 속도를 빠르게 해 준다.
  • 위와 같이 1x1 Conv Layer를 넣음으로써, Dimensionaly Reduction을 수행한다. 3x3 Max Pooling Layer를 추가한다. 이를 통해서 Another Option을 Inception Layer에 제공한다.
  • 3x3 Max Pooling Layer는 Inception Layer에 또 다른 옵션을 주기 위함이다.
  • Hebbian Principle - Human Learning에 대한 것
    • Neuron은 함께 Firing하며, 함께 Wiring된다.
    • 딥러닝 Layer를 만들때, 각 Layer는 이전 Layer의 정보에 집중합니다. 이전 Layer가 엉망이면..다음 Layer도 엉망입니다.
    • 이는 딥러닝 모델을 만들때, 각 Layer은 이전 Layer에 집중한다는 의미이다.어떠한 Layer가 우리의 Deep Learning Model에서 얼굴의 각 부분들에 집중했다고 가정해 보다. 다음 Layer는 아마도 다른 얼굴의 표현을 구분하기 위해서 전체적인 얼굴에 집중 할 것이라는 의미이다. 따라서 실제적으로 이것을 하기 위하여, Layer는 다른 Object들을 검출하기 위하여, 적절한 Filter Size를 가져야만 한다.
  • 1x1, 3x3, 5x5로 정한 것은 어떠한 연구에 의한 것이 아니라, 단순히 편의성 때문에 선택한 것이다.
  • Max Pooling도 역시 최근에는 인식률 증가에 의해서 거의 필수적이므로, 단지 한번 추가한 것이다. (이유는 모른다.)
  • 모든 layer에서(Inception Module에서도) Conv다음에 ReLU를 다 사용했다.
  • 224x224 RGB 이미지를 사용하고, mean subtration을 사용했다.
  • Projection Layer는 Max-Pooling다음에 1x1 Conv로써, Dimension을 맞추기 위해 사용했다. (Concatenation을 위하여)
  • NiN 논문을 차용하여, Average Pooling을 FCN전에 사용했다. (FCL만을 사용했을 때 보다 Top-1 Accuracy에서 0.6%향상을 보였다. 이는 NiN에서 설명되었듯, Adapting과 Fine-Tunning을 가능하게 했다.)

Training Method

  • 이 Architecture를 사용한 목적은, ImageNet Challenge에서 우승하기 위하여, 몇가지 부가적인 조작을 하였다.
    • Data Augmentation
    • Hyperparameter Setting for Challenge
    • Optimization Technique & Learning Rate Scheduling
    • Auxiliary Training
    • Ensembling Techniques
  • 이중에서 나머지는 걍 그렇고, Auxiliary Training은 꽤 흥미롭고 Novel하다. 그래서 이것을 좀 더 깊게 보려고 한다.
  • 중간 Network에 죽지 않도록 하기 위해서, Auxiliary Classifier(2개의 Softmax를 중간 중간에 넣었다)를 넣였다. 그들은 본질적으로 Auxiliary Loss를 같은 Label에 대해서 계산하고, 최종 로스에 Weighted sum으로 합쳐진다. (auxiliary loss * 0.3 + real loss * 0.7)
  • Inference Time에서 Auxiliaray Network는 제거된다.
  • 여러가지 방법을 썼는데,
    • Andrew Howard의 Photometric Distortion사용
    • Smaller and Larger Relative Crop 사용
    • Various Sized Patches Sampling사용(8% ~ 100%)
    • 다양한 랜덤 Aspect Ratio(3/4, 4/3 사용)
    • 그리고 다양한 Random Interpolation Method(Bilinear, area, nearest neighbor, cubic 등 )사용
    • Hyper parameter들 사용 등등
    • 어떠한 방법떄문에 Training이 최종 결과를 잘 낼수 있었는지는 모르겠다...라고 밝힘
  • Classification에서 Ensemble을 사용하였는데, 총 7개의 모델을 사용(6개는 같고, 1개는 좀 더 Wider함)
    • Training시에
      • 똑같은 초기 Weight값
      • 똑같은 Learning Rate Policy
      • 다른 Sampling Method와 다른 Random Ordering Selection of input images
    • Testing시에
      • 좀더 Aggressive한 Cropping Approach를 사용했다. (이미지를 4배 크게 했다. 좀더 짧은 쪽으로), 그리고 왼쪽, 가운데, 오른쪽 들의 Qaure를 Resized이미지로 사용했다.
      • 하나의 이미지를 Classification하기 위해서 144개(4*3*6*2)의 Crop된 이미지를 사용했다. (OMG)
      • Softmax Probabilities는 모든 이미지 Crop의 결과에 대해 평균했다.
        • Crop들에 대해서 Max Pooliing하고 Classifier에 대해서 Averaging해봤는데, 위의 결과에 대한 단순 평균보다 결과가 좋지 않았다...
        •  

 


Implementation (by Keras)

  • Cifar-10으로 가지고 Implementation을 해보자. Cifar는 32x32x3의 이미지로 60,000개의 Data가 있고 50,000개는 Training, 10,000개는 Testing Data로 구성되어 있다. Class는 10개 이다.

Code

import os

# os.environ["KERAS_BACKEND"] = 'plaidml.keras.backend'

import keras
import keras.backend as K

from keras.models import Model
from keras.layers import Conv2D, MaxPool2D, Dropout, Dense, Input, concatenate, GlobalAveragePooling2D, \
    AveragePooling2D, Flatten

import cv2
import numpy as np
from keras.datasets import cifar10
from keras.utils import np_utils

import math
from keras.optimizers import SGD
from keras.callbacks import LearningRateScheduler
import matplotlib

matplotlib.use('Agg')
# Loading dataset and Performing some preprocessing steps.

num_classes = 10


def load_cifar10_data(img_rows, img_cols):
    """
    Cifar Image를 다운로드 받아서
    이미지를 training과 valid로 나누고
    Preprocessing을 해 준다.
    :param img_rows: 리사이징 이미지 크기 (Row)
    :param img_cols: 리사이징 이미지 크기 (Col)
    :return: normalized된 train과 valid 이미지 numpy array
    """

    # load cifar-10 training and validation sets
    (x_train, y_train), (x_valid, y_valid) = cifar10.load_data()
    x_train = x_train[0:2000, :, :, :]
    y_train = y_train[0:2000]

    x_valid = x_valid[0:500, :, :, :]
    y_valid = y_valid[0:500]

    # resize training images
    x_train = np.array([cv2.resize(img, (img_rows, img_cols)) for img in x_train[:, :, :, :]])
    x_valid = np.array([cv2.resize(img, (img_rows, img_cols)) for img in x_valid[:, :, :, :]])

    # transform targets to keras compatible format
    y_train = np_utils.to_categorical(y_train, num_classes)
    y_valid = np_utils.to_categorical(y_valid, num_classes)

    x_train = x_train.astype('float32')
    x_valid = x_valid.astype('float32')

    # preprocess data (영상이미지라서 255.0으로 나눠서 normalize한다)
    x_train = x_train / 255.0
    x_valid = x_valid / 255.0

    return x_train, y_train, x_valid, y_valid


# define inception v1 architecture
def inception_module(x, filters_1x1, filters_3x3_reduce, filters_3x3, filters_5x5_reduce, filters_5x5, filters_pool_proj, name=None,
                     kernel_init='glorot_uniform', bias_init='zeros'):

    conv_1x1 = Conv2D(filters_1x1, (1, 1), padding='same', activation='relu', kernel_initializer=kernel_init, bias_initializer=bias_init)(x)

    conv_3x3_reduce = Conv2D(filters_3x3_reduce, (1, 1), padding='same', activation='relu', kernel_initializer=kernel_init, bias_initializer=bias_init)(x)

    conv_3x3 = Conv2D(filters_3x3, (3, 3), padding='same', activation='relu', kernel_initializer=kernel_init, bias_initializer=bias_init)(conv_3x3_reduce)

    conv_5x5_reduce = Conv2D(filters_5x5_reduce, (1, 1), padding='same', activation='relu', kernel_initializer=kernel_init, bias_initializer=bias_init)(x)

    conv_5x5 = Conv2D(filters_5x5, (5, 5), padding='same', activation='relu', kernel_initializer=kernel_init, bias_initializer=bias_init)(conv_5x5_reduce)

    max_pool = MaxPool2D((3, 3), strides=(1, 1), padding='same')(x)

    pool_proj = Conv2D(filters_pool_proj, (1, 1), padding='same', activation='relu', kernel_initializer=kernel_init, bias_initializer=bias_init)(max_pool)

    output = concatenate([conv_1x1, conv_3x3, conv_5x5, pool_proj], axis=3, name=name)

    return output


def decay(epoch, steps=100):
    initial_lrate = 0.01
    drop = 0.96
    epoch_drop = 8
    lrate = initial_lrate * math.pow(drop, math.floor((1 + epoch) / epoch_drop))
    return lrate


def main():
    print('Hello World!!')

    x_train, y_train, x_valid, y_valid = load_cifar10_data(224, 224)

    kernel_init = keras.initializers.glorot_uniform()

    bias_init = keras.initializers.Constant(value=0.2)

    input_layer = Input(shape=(224, 224, 3))

    # Layer 1
    x = Conv2D(64, (7, 7), padding='same', strides=(2, 2), activation='relu', name='conv_1_7x7/2', kernel_initializer=kernel_init, bias_initializer=bias_init)(input_layer)

    x = MaxPool2D((3, 3), strides=(2, 2), name='max_pool_1_3x3/2', padding='same')(x)

    # Layer 2
    x = Conv2D(192, (3, 3), padding='same', strides=(1, 1), activation='relu', name='conv_2_3x3/1', kernel_initializer=kernel_init, bias_initializer=bias_init)(x)

    x = MaxPool2D((3, 3), strides=(2, 2), name='max_pool_2_3x3/2', padding='same')(x)

    # Layer 3
    x = inception_module(x, 64, 96, 128, 16, 32, 32, name='inception_3a', kernel_init=kernel_init, bias_init=bias_init)
    x = inception_module(x, 128, 128, 192, 32, 96, 64, name='inception_3b', kernel_init=kernel_init, bias_init=bias_init)
    x = MaxPool2D((3, 3), strides=(2, 2), name='max_pool_3_3x3/2')(x)

    # Layer 4
    x = inception_module(x, 192, 96, 208, 16, 48, 64, name='inception_4a')

    # Layer 4 - Auxiliary Learning 1
    x1 = AveragePooling2D((5, 5), strides=3, name='avg_pool_aux_1')(x)
    x1 = Conv2D(128, (1, 1), padding='same', activation='relu', name='conv_aux_1')(x1)
    x1 = Flatten()(x1)
    x1 = Dense(1024, activation='relu', name='dense_aux_1')(x1)
    x1 = Dropout(0.7)(x1)
    x1 = Dense(10, activation='softmax', name='aux_output_1')(x1)

    x = inception_module(x, 160, 112, 224, 24, 64, 64, name='inception_4b', kernel_init=kernel_init, bias_init=bias_init)
    x = inception_module(x, 128, 128, 256, 24, 64, 64, name='inception_4c', kernel_init=kernel_init, bias_init=bias_init)
    x = inception_module(x, 112, 144, 288, 32, 64, 64, name='inception_4d', kernel_init=kernel_init, bias_init=bias_init)

    # Layer 4 - Auxiliary Learning 2
    x2 = AveragePooling2D((5, 5), strides=3, name='avg_pool_aux_2')(x)
    x2 = Conv2D(128, (1, 1), padding='same', activation='relu', name='conv_aux_2')(x2)
    x2 = Flatten()(x2)
    x2 = Dense(1024, activation='relu', name='dense_aux_2')(x2)
    x2 = Dropout(0.7)(x2)
    x2 = Dense(10, activation='softmax', name='aux_output_2')(x2)

    x = inception_module(x, 256, 160, 320, 32, 128, 128, name='inception_4e', kernel_init=kernel_init, bias_init=bias_init)
    x = MaxPool2D((3, 3), strides=(2, 2), name='max_pool_4_3x3/2')(x)

    # Layer 5
    x = inception_module(x, 256, 160, 320, 32, 128, 128, name='inception_5a', kernel_init=kernel_init, bias_init=bias_init)
    x = inception_module(x, 384, 192, 384, 48, 128, 128, name='inception_5b', kernel_init=kernel_init, bias_init=bias_init)
    x = GlobalAveragePooling2D(name='global_avg_pool_5_3x3/1')(x)
    x = Dropout(0.4)(x)
    x = Dense(10, activation='softmax', name='output')(x)

    model = Model(input_layer, [x, x1, x2], name='inception_v1')

    model.summary()

    epoch = 25
    initial_lrate = 0.01

    sgd = SGD(lr=initial_lrate, momentum=0.9, nesterov=False)
    lr_sc = LearningRateScheduler(decay, verbose=1)

    model.compile(loss=['categorical_crossentropy', 'categorical_crossentropy', 'categorical_crossentropy'],
                  loss_weights=[1, 0.3, 0.3], optimizer=sgd, metrics=['accuracy'])

    history = model.fit(x_train, [y_train, y_train, y_train], validation_data=(x_valid, [y_valid, y_valid, y_valid]),
                        epochs=epoch, batch_size=20, callbacks=[lr_sc])


if __name__ == '__main__':
    main()

 

 

 


Questions

  • 왜 3x3, 5x5, 1x1, Max Pooling을 Inception Module에서 사용했는가? 7x7 등을 사용하면 안되는가?
  • 왜 Max Pooling이후에 1x1로 Feature Map의 개수를 줄여줬는가?
  • 어떻게 Inception Module 안의 Max Pooling은 Inception Module에 Option을 주는가?
  • 어떻게 Inception Module이 각 Filter Size가 자동으로 선택될 수 있을까?
  • Average Pooling에 대해서 생각 해 보자. (NiN에서 사용함) 장단점은 뭘까?
  • 여기서 사용된, Some Improvements on deep convolutional neural network based image classification(2013), Andrew G. Howard.논문을 보고 Training하는 방법을 생각해보자. (Small Cropping, Larger Cropping Sampling Training)
  • GlobalAveragePooling2D에 대해서 생각해보자. AveragePooling과의 차이는 무엇일까?