'별거 없는' 시리즈는, 논문을 리뷰하고 코드로 구현해 보면서, 정말 논문과 코드 둘다 별거 없다는 것을 보여주기 위한 시리즈입니다. 사실상 대학들에서 나오는 논문들을 보면 쓸모없는 것 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이다.
▼
▼
▼
- 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
- 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를 만들 수 있다.
- 이를 통해서 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해봤는데, 위의 결과에 대한 단순 평균보다 결과가 좋지 않았다...
- Training시에
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과의 차이는 무엇일까?