(Inception-v4) Inception-v4, inception-resnet and the impact of residual connections on learning 번역 및 추가 설명과 Keras 구현

19 minute read

Paper Information

SZEGEDY, Christian, et al. “Inception-v4, inception-resnet and the impact of residual connections on learning”. In: Thirty-First AAAI Conference on Artificial Intelligence. 2017.

a.k.a. Inception-v4 paper


Abstract

매우 깊은 CNN 구조는, 최근 몇 년 동안의 image recognition 성능 발전에 중요한 역할을 했다. 비교적 낮은 계산 비용으로 상당히 우수한 성능을 달성한 Inception 구조도 그 중 하나에 해당한다.


최근에는 더 전통적인 CNN 구조에, residual connection을 도입한 연구가 있었으며, 이는 ILSVRC 2015에서 state-of-the-art 성능을 달성했다.

ResNet을 말한다. 이는 Inception 구조의 최신 버전인 Inception-v3와 유사한 성능에 해당하며, 이로부터 residual connectionInception 구조를 결합했을 때의 이점에 대한 의문이 생겨나게 된다.


이 논문에서는 residual connection을 통한 학습이, Inception network의 학습을 크게 가속화한다는 경험적 증거를 명확히 제시한다. 또한, residual Inception network가 유사한 비용의 non-residual Inception network보다 성능이 약간 좋다는 증거도 제시한다.

non-residualInception-v4를, residualInception-ResNet-v1Inception-ResNet-v2를 말한다.


논문에서는 residualnon-residual 두 버전의 Inception network를 위한 새로운 구조를 몇 가지 제시한다. 이러한 구조적 변화가 ILSVRC 2012 classification 분야에서 single-frame recognition 성능을 크게 향상시켰다.


또한, 적절한 activation scaling이 very wide residual Inception network의 학습 안정화에 얼마나 도움되는지 알아본다.


실험에서는 3개의 Inception-ResNet-v2와 1개의 Inception-v4를 ensemble하여 ImageNet classification의 test set에서 3.08%의 top-5 error를 얻어냈다.


1. Introduction

2012 ImageNet competion에서 AlexNet이 우승한 이래로, 이 네트워크는 컴퓨터 비전의 다양한 분야에 적용되어 성공적인 성과를 거뒀다.

이는 CNN 구조가 성공적으로 적용된 분야의 일부에 불과하다.


본 연구에서는, 최근에 발표 된 ResNetInception-v3의 두 아이디어를 결합하는 연구를 진행했다.


ResNet에서, deep architecture의 학습에는 residual connection이 본질적으로 중요한 것이라 주장했다.


Inception network는 일반적으로 very deep한 구조를 갖는다. 따라서 ResNet의 주장에 근거하면, Inception 구조의 filter contatenation 단계를 residual connection으로 교체하는 것이 자연스럽다.

이로써, Inception은 계산 효율성을 유지하면서도 residual approach의 이점을 모두 취할 수 있게 됐다.


또한, 둘의 단순한 결합 외에도, Inception 자체를 더 깊고 넓게 만들어서 보다 효율적이게 만들 수 있는가에 대한 연구도 진행했다. 이전 버전인 Inception-v3보다, 더 많은 Inception 모듈을 사용하면서도 단순하고 획일화 된 구조의 Inception-v4를 설계했다.


Inception-v3에서는, Inception 초기 버전의 baggage를 많이 이어받았었다. 기술적인 제약 사항은 주로 DistBelief로 분산 학습을 하기 위한 model partitioning에서 발생했다.

Baggage는 단점 정도로 보면 된다.


현재는 학습 환경을 TensorFlow에서 자체 개발한 분산 학습 프레임워크로 옮겼고, 위의 제약 사항들이 풀렸기 때문에 구조를 크게 단순화 할 수 있었다. 이 구조에 대해서는 3장에서 설명한다.


이 논문에서는 순수한 Inception의 변형인 Inception-v3Inception-v4, 그리고 유사한 비용의 하이브리드 버전인 Inception-ResNet을 비교한다.

Parameter나 computational complexity가 non-residual 모델과 유사해야한다는 주된 제약 사항을 고려한 임시적인 방법으로 만들어진 모델들이다.


실제로 더 크고 넓은 버전의 Inception-ResNet을 ImageNet classification dataset에서 테스트했다.


마지막 실험은 논문에서 제안한 최고 성능의 모델들을 모두 ensemble하여 평가하고 있다.


Inception-v4와 Inception-ResNet-v2가 유사한 성능을 보였으며, ImageNet validation dataset의 single frame에 대한 평가에서 state-of-the-art의 성능을 능가했다. 따라서, ImagaeNet과 같이 잘 연구 된 데이터에서, 이러한 조합들이 어떻게 state-of-the-art 성능을 달성하는지 알아보려 했다.


Ensemble 성능에서는 single frame에서의 성능 이득만큼 차이나지 않았다.

Table.2에 나온 single frame 성능에서, Inception-v3Inception-ResNet-v2의 차이는 top-5 error와 top-1 error에서 각각 0.7%, 1.3%씩 난다. 반면, Table.5의 enssemble 성능에서는 Inception-v3를 4개 ensemble한 것과 Inception-v4를 1개, Inception-ResNet-v2를 3개 ensemble의 차이는 각각 0.4%, 0.8%만 난다.


그럼에도, ensemble에서의 best 성능은 ImageNet validation set에 대한 top-5 error가 3.1%를 달성한다.


마지막 장에서는 classification에 실패한 경우의 일부에 대해 알아보고, ensemble이 여전히 dataset에 대한 label noise까지 도달하진 못했으며, 예측을 위한 개선 여지가 여전히 존재한다고 결론 내린다.


CNN은 AlexNet 이후부터 large scale image recognition 분야에서 널리 사용됐다.


그 다음으로 중요한 구조 몇 가지는 다음과 같다.


Residual connection은 ResNet에서 소개됐다. 여기서는 image recognition과 object detection 분야에서, signal의 additive merging을 활용함으로써 얻는 이점에 대해, 설득력 있는 이론적 및 실용적인 증거를 제시하고 있다.


Fig.1

Fig.1
ResNet에서 도입한 residual connection.


Fig.2

Fig.2
ResNet에서 제안한 비용이 최적화 버전의 residual connection.


ResNet의 저자들은 residual connection이 본질적으로 매우 깊은 CNN의 학습에 필요하다고 주장하고 있다. 하지만, 이 논문의 연구 결과에서는 최소한 image recognition에 대해서는 이 주장을 지지하지 못하는 것으로 보인다.

이러한 residual connection이 이득이 되는 범위를 이해하려면, deeper architecture에 대한 measurement point가 더 많이 필요해 보인다.


실험에서는 residual connection을 활용하지 않고도, 경쟁력 있는 성능의 very deep network를 학습 시키는 것이 그리 어렵지 않다는 것을 보여준다.

Inception-v4를 말함.


하지만, residual connection을 사용하면 학습 속도가 크게 향상되는 것으로 보이며, 이는 residual connection의 활용에 대한 큰 이유가 된다.


Inception deep convolutional architecture는 GoogLeNet에서 소개됐으며, 이는 Inception-v1 이라고도 칭했다. 이후의 Inception 구조는 다양한 방법으로 개선 됐으며, 처음에는 batch normalization을 이용한 Inception-v2 구조가 제안됐다. 그 다음으로는 factorization 기법의 추가를 통해 개선 된 Inception-v3 구조가 제안됐다.


3. Architectural Choices

3.1 Pure Inception blocks

이전의 Inception 모델은 분할 방식으로 학습됐다.

각 복제본(replica)은, 전체 모델이 메모리에 올라갈 수 있도록 여러 개의 sub-network로 분할됐다.


Inception 구조는 고도로 튜닝될 수 있다. 즉, 학습 후의 네트워크 성능에 영향을 미치지 않는 layer들의 filter 개수에 많은 변화를 줄 수 있다.


여러 sub-network 간의 계산적인 균형을 위해 layer의 크기를 조심스럽게 튜닝했으며, 이를 통해 학습 속도를 최적화시켰다.

이전 버전에선 그랬다고 함.


이와 대조적으로, TensorFlow 분산 학습 프레임워크를 도입하면, 복제본의 분할하지 않고도 가장 최근의 모델을 학습시킬 수 있다.

이는 gradient 계산에 필요한 tensor를 신중하게 고려하고, 이러한 tensor를 줄이기 위한 계산적인 구조화를 통해 부분적으로 가능해진다. 즉, backpropation에 사용된 메모리의 recent optimization으로 가능하게 된다.


저자들의 과거 연구에서는 구조적인 변경에 대해 보수적이었으며, 제한 된 실험을 했었다.

네트워크의 안정을 유지하면서, 일부 구성 요소들에 변화를 주기 위함.


또한, 네트워크 초반부의 구조를 단순화하지 않으면 필요 이상으로 복잡해보였다고 한다. 그래서 이번 Inception-v4의 실험에서는, 이런 불필요한 baggage를 버리고 각 grid size에 대한 Inception block의 구조를 획일화 시켰다.


Inception-v4의 전체적인 구조는 Fig.3를, 각 Incepiton modlue의 자세한 구조는 Fig.4 ~ Fig.9을 참조하자.

Inception module의 구조에서, ‘V’라고 표시 된 convolution은 padding='valid'를 뜻한다.


Fig.3

Fig.3
Inception-v4의 전체 구조에 대한 개요다.


Fig.4

Fig.4
Inception-v4Inception-ResNet-v2의 입력 부분에 사용되며, Fig.3의 Stem에 해당한다. 우측의 shape은 해당 layer의 출력 shape을 뜻한다.


Fig.5

Fig.5
Inception-v4에서 grid size가 \(35\times 35\)일 때 사용되는 Inception block이다. Fig.3의 Inception-A에 해당한다.


Fig.6

Fig.6
Inception-v4에서 grid size가 \(17\times 17\)일 때 사용되는 Inception block이다. Fig.3의 Inception-B에 해당한다.


Fig.7

Fig.7
Inception-v4에서 grid size가 \(8\times 8\)일 때 사용되는 Inception block이다. Fig.3의 Inception-C에 해당한다.


Fig.8

Fig.8
Inception-v4에서 grid size를 \(35\times 35\)에서 \(17\times 17\)로 줄일 때 사용하는 reduction module이며, Fig.3과 Fig.10의 Reduction-A에 해당한다. 각 conv layer에 사용되는 filter 수는 모델마다 다르며, 각 conv filter 수인 \(k, l, m, n\)은 Table.1을 따른다.


Table.1

Table.1
각 Inception 버전에 사용되는 Reduction-A의 filter 개수를 나타낸다.


Fig.9

Fig.9
Inception-v4에서 grid size를 \(17\times 17\)에서 \(8\times 8\)로 줄일 때 사용하는 reduction module이며, Fig.3의 Reduction-B에 해당한다.


이번에는 Inception-v4를 Keras로 구현해보자. Fig.4Stem은 다음과 같이 구현하면 다음과 같다.

def Stem(input_tensor, version=None, name=None):
    if version == 'Inception-v4' or version == 'Inception-ResNet-v2':
        x = conv2d_bn(input_tensor, 32, (3, 3), padding='valid', strides=2) # 299x299x3 -> 149x149x32
        x = conv2d_bn(x, 32, (3, 3), padding='valid') # 149x149x32 -> 147x147x32
        x = conv2d_bn(x, 64, (3, 3)) # 147x147x32 -> 147x147x64
        
        branch_1 = MaxPooling2D((3, 3), padding='valid', strides=2)(x)
        branch_2 = conv2d_bn(x, 96, (3, 3), padding='valid', strides=2)
        x = Concatenate()([branch_1, branch_2]) # 73x73x160
        
        branch_1 = conv2d_bn(x, 64, (1, 1))
        branch_1 = conv2d_bn(branch_1, 96, (3, 3), padding='valid')
        branch_2 = conv2d_bn(x, 64, (1, 1))
        branch_2 = conv2d_bn(branch_2, 64, (7, 1))
        branch_2 = conv2d_bn(branch_2, 64, (1, 7))
        branch_2 = conv2d_bn(branch_2, 96, (3, 3), padding='valid')
        x = Concatenate()([branch_1, branch_2]) # 71x71x192
        
        branch_1 = conv2d_bn(x, 192, (3, 3), padding='valid', strides=2) # Fig.4 is wrong
        branch_2 = MaxPooling2D((3, 3), padding='valid', strides=2)(x)
        x = Concatenate(name=name)([branch_1, branch_2]) if name else Concatenate()([branch_1, branch_2]) # 35x35x384
        
    elif version == 'Inception-ResNet-v1':
        x = conv2d_bn(input_tensor, 32, (3, 3), padding='valid', strides=2) # 299x299x3 -> 149x149x32
        x = conv2d_bn(x, 32, (3, 3), padding='valid') # 149x149x32 -> 147x147x32
        x = conv2d_bn(x, 64, (3, 3)) # 147x147x32 -> 147x147x64
        
        x = MaxPooling2D((3, 3), strides=2, padding='valid')(x) # 147x147x64 -> 73x73x64
        
        x = conv2d_bn(x, 80, (1, 1)) # 73x73x64 -> 73x73x80
        x = conv2d_bn(x, 192, (3, 3), padding='valid') # 73x73x80 -> 71x71x192U
        x = conv2d_bn(x, 256, (3, 3), padding='valid', strides=2, name=name) # 71x71x192 -> 35x35x256
        
    else:
        return None # Kill ^^
    
    return x

Stem의 경우에는 Inception-v4Inception-ResNet-v2가 구조를 공유하고, Inception-ResNet-v1은 다른 구조를 사용한다. 3.2절의 residual 버전의 구현에서도 위의 Stem 함수를 사용한다.

Fig.4의 Stem 구조에는 잘못된 부분이 있으며, 해당 코드의 후방에 주석으로 표시했다.

Fig.4의 \(3\times 3\) conv layer에서 192 V라고 나와있지만, 이렇게 수행되면 grid size에 차이가 생긴다. 다음 layer인 Filter concat의 shape에 맞춘다면 192 stride 2 V가 맞다.


Inception-v4에서 사용하는 Inception module인 Fig.5 ~ Fig.7을 구현하면 다음과 같다.

def Inception_A(input_tensor, name=None):
    branch_1 = AveragePooling2D((3, 3), strides=1, padding='same')(input_tensor)
    branch_1 = conv2d_bn(branch_1, 96, (1, 1))
    
    branch_2 = conv2d_bn(input_tensor, 96, (1, 1))

    branch_3 = conv2d_bn(input_tensor, 64, (1, 1))
    branch_3 = conv2d_bn(branch_3, 96, (3, 3))
    
    branch_4 = conv2d_bn(input_tensor, 64, (1, 1))
    branch_4 = conv2d_bn(branch_4, 96, (3, 3))
    branch_4 = conv2d_bn(branch_4, 96, (3, 3))
    
    filter_concat = Concatenate(name=name)([branch_1, branch_2, branch_3, branch_4]) if name else Concatenate()([branch_1, branch_2, branch_3, branch_4])
    
    return filter_concat

def Inception_B(input_tensor, name=None):
    branch_1 = AveragePooling2D((3, 3), strides=1, padding='same')(input_tensor)
    branch_1 = conv2d_bn(branch_1, 128, (1, 1))
    
    branch_2 = conv2d_bn(input_tensor, 384, (1, 1))
    
    branch_3 = conv2d_bn(input_tensor, 192, (1, 1))
    branch_3 = conv2d_bn(branch_3, 224, (1, 7))
    branch_3 = conv2d_bn(branch_3, 256, (7, 1)) # Fig.6 is wrong
    
    branch_4 = conv2d_bn(input_tensor, 192, (1, 1))
    branch_4 = conv2d_bn(branch_4, 192, (1, 7))
    branch_4 = conv2d_bn(branch_4, 224, (7, 1))
    branch_4 = conv2d_bn(branch_4, 224, (1, 7))
    branch_4 = conv2d_bn(branch_4, 256, (7, 1))
    
    filter_concat = Concatenate(name=name)([branch_1, branch_2, branch_3, branch_4]) if name else Concatenate()([branch_1, branch_2, branch_3, branch_4])
    
    return filter_concat

def Inception_C(input_tensor, name=None):
    branch_1 = AveragePooling2D((3, 3), strides=1, padding='same')(input_tensor)
    branch_1 = conv2d_bn(branch_1, 256, (1, 1))
    
    branch_2 = conv2d_bn(input_tensor, 256, (1, 1))

    branch_3 = conv2d_bn(input_tensor, 384, (1, 1))
    branch_3a = conv2d_bn(branch_3, 256, (1, 3))
    branch_3b = conv2d_bn(branch_3, 256, (3, 1))
    branch_3 = Concatenate()([branch_3a, branch_3b])
    
    branch_4 = conv2d_bn(input_tensor, 384, (1, 1))
    branch_4 = conv2d_bn(branch_4, 448, (1, 3))
    branch_4 = conv2d_bn(branch_4, 512, (3, 1))
    branch_4a = conv2d_bn(branch_4, 256, (1, 3))
    branch_4b = conv2d_bn(branch_4, 256, (3, 1))
    branch_4 = Concatenate()([branch_4a, branch_4b])
    
    filter_concat = Concatenate(name=name)([branch_1, branch_2, branch_3, branch_4]) if name else Concatenate()([branch_1, branch_2, branch_3, branch_4])
    
    return filter_concat

Fig.6Inception-B 구조에도 잘못된 부분이 있으며, 해당 코드의 후방에 주석으로 표시했다.

3번째 branch에서 \(1\times 1\) conv layer 뒤에는 \(1\times 7\) conv layer와 \(7\times 1\) conv layer가 순서대로 와야하지만, Fig.6에는 \(1\times 7\) 다음에도 \(1\times 7\) conv layer가 오고있다.


Inception-v4에서 사용하는 reduction module인 Fig.8 ~ Fig.9를 구현하면 다음과 같다.

reduction_table = {'Inception-v4' : [192, 224, 256, 384],
                   'Inception-ResNet-v1' : [192, 192, 256, 384],
                   'Inception-ResNet-v2' : [256, 256, 384, 384]}

def Reduction_A(input_tensor, version=None, name=None):
    k, l, m, n = reduction_table[version]

    branch_1 = MaxPooling2D((3, 3), padding='valid', strides=2)(input_tensor)

    branch_2 = conv2d_bn(input_tensor, n, (3, 3), padding='valid', strides=2)

    branch_3 = conv2d_bn(input_tensor, k, (1, 1))
    branch_3 = conv2d_bn(branch_3, l, (3, 3))
    branch_3 = conv2d_bn(branch_3, m, (3, 3), padding='valid', strides=2)

    filter_concat = Concatenate(name=name)([branch_1, branch_2, branch_3]) if name else Concatenate()([branch_1, branch_2, branch_3])

    return filter_concat

def Reduction_B(input_tensor, version=None, name=None):
    if version == 'Inception-v4':
        branch_1 = MaxPooling2D((3, 3), padding='valid', strides=2)(input_tensor)
    
        branch_2 = conv2d_bn(input_tensor, 192, (1, 1))
        branch_2 = conv2d_bn(branch_2, 192, (3, 3), padding='valid', strides=2)
    
        branch_3 = conv2d_bn(input_tensor, 256, (1, 1))
        branch_3 = conv2d_bn(branch_3, 256, (1, 7))
        branch_3 = conv2d_bn(branch_3, 320, (7, 1))
        branch_3 = conv2d_bn(branch_3, 320, (3, 3), padding='valid', strides=2)
    
        filter_concat = Concatenate(name=name)([branch_1, branch_2, branch_3]) if name else Concatenate()([branch_1, branch_2, branch_3])

    elif version == 'Inception-ResNet-v1':
        branch_1 = MaxPooling2D((3, 3), padding='valid', strides=2)(input_tensor)
    
        branch_2 = conv2d_bn(input_tensor, 256, (1, 1))
        branch_2 = conv2d_bn(branch_2, 384, (3, 3), padding='valid', strides=2)
    
        branch_3 = conv2d_bn(input_tensor, 256, (1, 1))
        branch_3 = conv2d_bn(branch_3, 256, (3, 3), padding='valid', strides=2)
        
        branch_4 = conv2d_bn(input_tensor, 256, (1, 1))
        branch_4 = conv2d_bn(branch_4, 256, (3, 3))
        branch_4 = conv2d_bn(branch_4, 256, (3, 3), padding='valid', strides=2)
    
        filter_concat = Concatenate(name=name)([branch_1, branch_2, branch_3, branch_4]) if name else Concatenate()([branch_1, branch_2, branch_3, branch_4])

    elif version == 'Inception-ResNet-v2':
        branch_1 = MaxPooling2D((3, 3), padding='valid', strides=2)(input_tensor)
    
        branch_2 = conv2d_bn(input_tensor, 256, (1, 1))
        branch_2 = conv2d_bn(branch_2, 384, (3, 3), padding='valid', strides=2)
    
        branch_3 = conv2d_bn(input_tensor, 256, (1, 1))
        branch_3 = conv2d_bn(branch_3, 288, (3, 3), padding='valid', strides=2)
        
        branch_4 = conv2d_bn(input_tensor, 256, (1, 1))
        branch_4 = conv2d_bn(branch_4, 288, (3, 3))
        branch_4 = conv2d_bn(branch_4, 320, (3, 3), padding='valid', strides=2)
    
        filter_concat = Concatenate(name=name)([branch_1, branch_2, branch_3, branch_4]) if name else Concatenate()([branch_1, branch_2, branch_3, branch_4])
    
    else:
        return None # Kill ^^
    
    return filter_concat

Reduction-A의 경우에는 Inception-v4Inception-ResNet-v1, Inception-ResNet-v2가 모두 구조를 공유하며, filter의 개수만 Table.1을 따른다. 이는 reduction_table이라는 이름으로 딕셔너리 변수를 선언해서 사용하고 있다.

Reduction-B의 경우에는 각 모델이 다른 구조를 사용하지만, 블록의 이름이 같기 때문에 하나의 함수로 구현했다.

Inception-ResNet-v1Inception-ResNet-v2의 경우에는 구조가 동일하지만, filter의 개수에 차이가 있다.


위의 모듈들을 이용하는 Inception-v4 구조인 Fig.3을 구현하면 다음과 같다.

def Inception_v4(model_input, classes=1000):
    version = 'Inception-v4'
    
    x = Stem(model_input, version=version, name='Stem') # (299, 299, 3) -> (35, 35, 384)
    
    for i in range(4):
        x = Inception_A(x, name='Inception-A-'+str(i+1)) # (35, 35, 384)
    
    x = Reduction_A(x, version=version, name='Reduction-A') # (35, 35, 384) -> (17, 17, 1024)
    
    for i in range(7):
        x = Inception_B(x, name='Inception-B-'+str(i+1)) # (17, 17, 1024)

    x = Reduction_B(x, version=version, name='Reduction-B') # (17, 17, 1024) -> (8, 8, 1536)
    
    for i in range(3):
        x = Inception_C(x, name='Inception-C-'+str(i+1)) # (8, 8, 1536)
    
    x = GlobalAveragePooling2D()(x) # (1536)
    x = Dropout(0.8)(x)
    
    model_output = Dense(classes, activation='softmax', name='output')(x)

    model = Model(model_input, model_output, name='Inception-v4')
    
    return model


3.2 Residual Inception Blocks

Residual 버전의 Inception network에서는, 기존의 Inception에서 사용된 것보다 더 저렴한 비용의 Inception block을 사용한다.


각 Inception block 뒤에는, filter bank의 dimension을 입력의 depth에 맞추기 위한 filter-expansion layer가 사용된다.

Activation이 없는 1x1 conv layer에 해당하며, 이는 Inception block에 의한 dimensionality reduction을 보완하기 위함이다. Inception-v3에서 언급한 Avoid Representational Bottlenecks이 목적인 것으로 보인다. 이전 포스트의 2장 참조.


Residual Inception에 대한 여러 버전을 시도했으며, 그 중 2가지에 대해서만 자세하게 설명한다. Inception-ResNet-v1의 비용은 대략 Inception-v3과 유사하며, Inception-ResNet-v2의 비용은 3.1절의 Inception-v4와 일치한다.

실제로 Inception-v4는 더 많은 layer의 수로 인해, 학습 속도가 더 느린 것으로 판명됐다.


두 버전의 전체적인 구조는 Fig.10를, Inception-ResNet-v1Inception-ResNet-v2에 사용 된 Inception module은 각각 Fig.11 ~ Fig.15Fig.16 ~ Fig.19를 참조하자.

Inception-ResNet-v1Reduction-A는 Fig.8을 따르며, Inception-ResNet-v2Stem은 Fig.4를, Reduction-A는 Fig.8을 따른다.


Fig.10

Fig.10
Inception-ResNet-v1Inception-ResNet-v2의 전체 구조에 대한 개요다.

각 block의 우측에 표시된 shape은 Inception-ResNet-v1 기준이다.


Inception-ResNet-v1에서 사용 된 각 모듈의 자세한 구조는 다음과 같다.


Fig.11

Fig.11
Inception-ResNet-v1의 입력 부분에 사용되며, Fig.10의 Stem에 해당한다. 우측의 shape은 해당 layer의 출력 shape을 뜻한다.


Fig.12

Fig.12
Inception-ResNet-v1에서 grid size가 \(35\times 35\)일 때 사용되는 Inception block이다. Fig.10의 Inception-ResNet-A에 해당한다.


Fig.13

Fig.13
Inception-ResNet-v1에서 grid size가 \(17\times 17\)일 때 사용되는 Inception block이다. Fig.10의 Inception-ResNet-B에 해당한다.


Fig.14

Fig.14
Inception-ResNet-v1에서 grid size를 \(17\times 17\)에서 \(8\times 8\)로 줄일 때 사용하는 reduction module이며, Fig.10의 Reduction-B에 해당한다.


Fig.15

Fig.15
Inception-ResNet-v1에서 grid size가 \(8\times 8\)일 때 사용되는 Inception block이다. Fig.10의 Inception-ResNet-C에 해당한다.



Inception-ResNet-v2에서 사용 된 각 모듈의 자세한 구조는 다음과 같다.


Fig.16

Fig.16
Inception-ResNet-v2에서 grid size가 \(35\times 35\)일 때 사용되는 Inception block이다. Fig.10의 Inception-ResNet-A에 해당한다.


Fig.17

Fig.17
Inception-ResNet-v2에서 grid size가 \(17\times 17\)일 때 사용되는 Inception block이다. Fig.10의 Inception-ResNet-B에 해당한다.


Fig.18

Fig.18
Inception-ResNet-v2에서 grid size를 \(17\times 17\)에서 \(8\times 8\)로 줄일 때 사용하는 reduction module이며, Fig.10의 Reduction-B에 해당한다.


Fig.19

Fig.19
Inception-ResNet-v2에서 grid size가 \(8\times 8\)일 때 사용되는 Inception block이다. Fig.10의 Inception-ResNet-C에 해당한다.


Residual 버전과 non-residual 버전 간의 또 다른 기술적 차이는, BN을 traiditional layer에서만 사용됐으며, summation에는 사용하지 않았다.


BN을 충분히 사용하는 것이 이득이긴 하지만, 각 모델의 복제본을 single GPU 상에서 유지하기 위함이다.

큰 activation size를 가진 layer가 차지하는 메모리 공간은 GPU memory를 불균형하게 소비하는 것으로 밝혀졌다고 한다. 이러한 layer들 위의 BN을 생략함으로써, Inception block의 수를 크게 늘릴 수 있었다.

컴퓨팅 리소스를 보다 효율적으로 활용해서, 이런 trade-off가 필요 없어졌으면 하는 바램이 있다더라.


3.3 Scaling of the Residuals

Filter 개수가 1000개를 초과하게 되면 residual variant가 불안정해지기 시작하며, 네트워크가 학습 초기에 죽어버리는 것으로 나타났다. 이는 learning rate를 낮추거나, BN을 추가하는 것으로는 예방할 수 없다.

수만 번의 iteration 이후부터 average pooling 이전의 마지막 layer에서는 0만을 출력했다고 한다.

Residual variant는 Inception-ResNet 버전을 말하는 것으로 보인다.


Residual을 누적 된 layer activation에 추가하기 전에 scaling down을 하는 것이 학습의 안정화에 도움되는 것처럼 보였다. 이를 위한 scaling factor는 0.1에서 0.3 사이의 값을 사용했다. 구조는 Fig.20을 참조하자.


Fig.20

Fig.20
Inception-ResNet modulescaling을 위한 general schema이다.

Scaling block은 마지막 linear activation을 적절한 상수에 대해 scaling한다. 일반적으로 적절한 scaling factor는 약 0.1 정도라 한다.

Inception block 대신, 임의의 subnetwork를 사용하는 일반적인 ResNet의 경우에도 이 아이디어가 유용할 것으로 보인다고 한다.


ResNet에서는 very deep residual network의 경우에도 비슷한 불안정성을 관찰했고, 이를 위해 2단계로 learning rate를 스케쥴링 했다.

첫 단계인 “warm-up” 단계에서는 매우 낮은 learning rate로 학습하다가, 두 번째 단계에서는 높은 learning rate로 학습한다.

이전 포스팅의 4.2절 CIFAR-10에 대한 실험에서 110-layer로 학습하는 경우에 해당한다.


저자들은 filter의 수가 매우 많은 경우에는 0.00001의 매우 낮은 learning rate조차도 불안정성에 대처하기에 충분하지 않으며, 높은 learning rate로 학습한다면 이를 제거할 기회를 가지게 된다는 것을 알아냈다.


저자들은 또한, 이 방법 대신 residual을 scaling하는 것이 훨씬 더 안정적이라는 것을 알아냈다.이러한 scaling이 엄밀히 꼭 필요한 것은 아니며, 최종 성능에 해를 끼치지 않으면서 학습의 안정화에 도움이 되는 것이라 한다.


이번에는 Inception-ResNet-v1Inception-ResNet-v2에서 사용하는 Inception module을 차례대로 구현해보자. 단순 구현이므로, 3.2절의 마지막에서 언급한 trade-off를 가볍게 무시하고 BN을 사용하도록 한다.


우선 residual 버전에서 사용하는 scaling downFig.20을 구현하면 다음과 같다.

def Scaling_Residual(Inception, scale):
    x = Lambda(lambda Inception, scale: Inception * scale, arguments={'scale': scale})(Inception)
    x = Activation(activation='relu')(x)
    
    return x

Connection을 위한 addition 연산 전에 수행된다.


Inception-ResNet-v1Inception-ResNet-v2에서 사용하는 Inception module인 {Fig.12 ~ Fig.19} - {Fig.14, Fig.18}를 구현하면 다음과 같다.

def Inception_ResNet_A(input_tensor, scale=0.1, version=None, name=None):   
    if version == 'Inception-ResNet-v1':
        branch_1 = conv2d_bn(input_tensor, 32, (1, 1))
    
        branch_2 = conv2d_bn(input_tensor, 32, (1, 1))
        branch_2 = conv2d_bn(branch_2, 32, (3, 3))
        
        branch_3 = conv2d_bn(input_tensor, 32, (1, 1))
        branch_3 = conv2d_bn(branch_3, 32, (3, 3))
        branch_3 = conv2d_bn(branch_3, 32, (3, 3))
        
        branches = Concatenate()([branch_1, branch_2, branch_3])
        Inception = conv2d_bn(branches, 256, (1, 1), activation=None)
    
    elif version == 'Inception-ResNet-v2':
        branch_1 = conv2d_bn(input_tensor, 32, (1, 1))
    
        branch_2 = conv2d_bn(input_tensor, 32, (1, 1))
        branch_2 = conv2d_bn(branch_2, 32, (3, 3))
        
        branch_3 = conv2d_bn(input_tensor, 32, (1, 1))
        branch_3 = conv2d_bn(branch_3, 48, (3, 3))
        branch_3 = conv2d_bn(branch_3, 64, (3, 3))
        
        branches = Concatenate()([branch_1, branch_2, branch_3])
        Inception = conv2d_bn(branches, 384, (1, 1), activation=None)
    
    else:
        return None # Kill ^^
    
    scaled_activation = Scaling_Residual(Inception, scale=scale)
    
    residual_connection = Add(name=name)([input_tensor, scaled_activation]) if name else Add()([input_tensor, scaled_activation])
    
    return residual_connection

def Inception_ResNet_B(input_tensor, scale=0.1, version=None, name=None):
    if version == 'Inception-ResNet-v1':
        branch_1 = conv2d_bn(input_tensor, 128, (1, 1))
        
        branch_2 = conv2d_bn(input_tensor, 128, (1, 1))
        branch_2 = conv2d_bn(branch_2, 128, (1, 7))
        branch_2 = conv2d_bn(branch_2, 128, (7, 1))
        
        branches = Concatenate()([branch_1, branch_2])
        Inception = conv2d_bn(branches, 896, (1, 1), activation=None)
    
    elif version == 'Inception-ResNet-v2':
        branch_1 = conv2d_bn(input_tensor, 192, (1, 1))
        
        branch_2 = conv2d_bn(input_tensor, 128, (1, 1))
        branch_2 = conv2d_bn(branch_2, 160, (1, 7))
        branch_2 = conv2d_bn(branch_2, 192, (7, 1))
        
        branches = Concatenate()([branch_1, branch_2])
        Inception = conv2d_bn(branches, 1152, (1, 1), activation=None) # Fig.17 is wrong
    
    else:
        return None # Kill ^^
    
    scaled_activation = Scaling_Residual(Inception, scale=scale)
    
    residual_connection = Add(name=name)([input_tensor, scaled_activation]) if name else Add()([input_tensor, scaled_activation])
    
    return residual_connection

def Inception_ResNet_C(input_tensor, scale=0.1, version=None, name=None):    
    if version == 'Inception-ResNet-v1':
        branch_1 = conv2d_bn(input_tensor, 192, (1, 1))
        
        branch_2 = conv2d_bn(input_tensor, 192, (1, 1))
        branch_2 = conv2d_bn(branch_2, 192, (1, 3))
        branch_2 = conv2d_bn(branch_2, 192, (3, 1))
        
        branches = Concatenate()([branch_1, branch_2])
        Inception = conv2d_bn(branches, 1792, (1, 1), activation=None)
    
    elif version == 'Inception-ResNet-v2':
        branch_1 = conv2d_bn(input_tensor, 192, (1, 1))
        
        branch_2 = conv2d_bn(input_tensor, 192, (1, 1))
        branch_2 = conv2d_bn(branch_2, 224, (1, 3))
        branch_2 = conv2d_bn(branch_2, 256, (3, 1))
        
        branches = Concatenate()([branch_1, branch_2])
        Inception = conv2d_bn(branches, 2144, (1, 1), activation=None) # Fig.19 is wrong
    
    else:
        return None # Kill ^^
    
    scaled_activation = Scaling_Residual(Inception, scale=scale)
    
    residual_connection = Add(name=name)([input_tensor, scaled_activation]) if name else Add()([input_tensor, scaled_activation])
    
    return residual_connection

이번에도 Inception_ResNet_v2Inception_ResNet_BInception_ResNet_CFig.17Fig.19에 잘못된 부분이 있다. 입력과 출력의 shape을 고려하면 위의 구현이 맞다.


위의 모듈들을 이용하는 Inception-ResNet-v1Inception-ResNet-v2의 구조인 Fig.10을 구현하면 다음과 같다.

def Inception_ResNet(model_input, version='Inception-ResNet-v2', classes=1000):    
    x = Stem(model_input, version=version, name='Stem')
    # Inception-ResNet-v1 : (299, 299, 3) -> (35, 35, 256)
    # Inception-ResNet-v2 : (299, 299, 3) -> (35, 35, 384)
    
    for i in range(5):
        x = Inception_ResNet_A(x, scale=0.17, version=version, name='Inception-ResNet-A-'+str(i+1))
        # Inception-ResNet-v1 : (35, 35, 256)
        # Inception-ResNet-v2 : (35, 35, 384)
        
    x = Reduction_A(x, version=version, name='Reduction-A')
    # Inception-ResNet-v1 : (35, 35, 256) -> (17, 17, 896)
    # Inception-ResNet-v2 : (35, 35, 384) -> (17, 17, 1152)
    
    for i in range(10):
        x = Inception_ResNet_B(x, scale=0.1, version=version, name='Inception-ResNet-B-'+str(i+1))
        # Inception-ResNet-v1 : (17, 17, 896)
        # Inception-ResNet-v2 : (17, 17, 1152)

    x = Reduction_B(x, version=version, name='Reduction-B')
    # Inception-ResNet-v1 : (17, 17, 896) -> (8, 8, 1792)
    # Inception-ResNet-v2 : (17, 17, 1152) -> (8, 8, 2144)
    
    for i in range(5):
        x = Inception_ResNet_C(x, scale=0.2, version=version, name='Inception-ResNet-C-'+str(i+1))
        # Inception-ResNet-v1 : (8, 8, 1792)
        # Inception-ResNet-v2 : (8, 8, 2144)
    
    x = GlobalAveragePooling2D()(x)
    # Inception-ResNet-v1 : (1792)
    # Inception-ResNet-v2 : (2144)
    
    x = Dropout(0.8)(x)
    
    model_output = Dense(classes, activation='softmax', name='output')(x)

    model = Model(model_input, model_output, name=version)
    
    return model

학습 코드는 이전 포스트의 Inception-v3 학습 코드에서, label smoothing과 auxiliary_classifier 부분만 제외하면 동일하기 때문에 생략한다.

Scaling factor는 Keras github을 참조했다. 여기에 구현 된 Inception-ResNet-v2는 논문과는 다른 구조로 구현되어 있다.


4. Training Methodology

20개의 복제본(replica)이 각각 NVidia Kepler GPU에서 수행되도록 [TensorFlow 분산 학습 시스템]을 사용하여 stochastic gradient로 학습했다.


실험 초기에는 decay가 0.9인 momentum을 사용했었고, best 성능은 decay가 0.9, \(\epsilon\)이 1.0인 RMSProp을 사용하여 달성했다.


Learning rate는 0.045부터 시작하여, 2번의 epoch마다 0.94를 곱했다.


모델 평가는 시간이 지남에 따라 계산 된 parameter들의 running average로 수행됐다.


5. Experimental Results

우선 4가지 버전의 학습 중 top-1 및 top-5 validation error를 관찰한다.


Bounding box의 질이 좋지 못한 약 1700개의 blacklisted entity를 생략한 validation set의 subset에 대해 지속적으로 평가했었음을 실험 후에 발견했다.

Blacklisted entity의 생략은 CLS-LOC benchmark에 대해서만 이뤄졌어야 했었다.


그럼에도, 저자들의 이전 연구를 포함한 다른 연구들과는 비교할 수 없을 정도의 긍정적 수치를 얻었다.

성능의 차이는 top-1와 top-5 error에서 각각 0.3%, 0.15% 정도였으며, 그 차이가 일관적이었기 때문에 성능 그래프 간의 비교가 공정한 것으로 본다고 한다.


Fig.21

Fig.21
계산 비용이 거의 동일한 Inception-v3Inception-ResNet-v1의 top-5 error 비교. 각 성능은 ILSVRC 2012의 non-blacklisted validation set에 대한 single-crop으로 측정됐다.

Residual 버전의 학습 속도가 빠르며, Inception-v3보다 성능이 약간 좋은 것으로 나타났다.


Fig.22

Fig.22
계산 비용이 거의 동일한 Inception-v3Inception-ResNet-v1의 top-1 error 비교. 각 성능은 ILSVRC 2012의 non-blacklisted validation set에 대한 single-crop으로 측정됐다.

Residual 버전의 학습 속도가 빠르며, Inception-v3보다 성능이 약간 좋지 않은 것으로 나타났다.


Fig.23

Fig.23
계산 비용이 거의 동일한 Inception-v4Inception-ResNet-v2의 top-5 error 비교. 각 성능은 ILSVRC 2012의 non-blacklisted validation set에 대한 single-crop으로 측정됐다.

Residual 버전의 학습 속도가 빠르며, Inception-v4보다 성능이 약간 좋은 것으로 나타났다.


Fig.24

Fig.24
계산 비용이 거의 동일한 Inception-v4Inception-ResNet-v2의 top-1 error 비교. 각 성능은 ILSVRC 2012의 non-blacklisted validation set에 대한 single-crop으로 측정됐다.

Residual 버전의 학습 속도가 빠르며, Inception-v4보다 성능이 약간 좋은 것으로 나타났다.


Fig.25

Fig.25
Inception-v3, Inception-v4, Inception-ResNet-v1, Inception-ResNet-v2 4가지 모델의 top-5 error 비교.

Residual 버전의 학습 속도가 빠르며, 모델의 크기에 따라 최종 성능이 달라지는 것으로 보인다.


Fig.26

Fig.26
Inception-v3, Inception-v4, Inception-ResNet-v1, Inception-ResNet-v2 4가지 모델의 top-5 error 비교.

Fig.26의 top-5 evaluation과 흡사한 결과를 보인다.


반면, 50000개의 이미지로 구성 된 validation set에 대해, multi-crop 및 ensemble 결과는 재수행했다. 또한, 최종 ensemble 결과는 test set에 대해 수행된 후, 검증을 위해 ILSVRC test server에 전송하고 overfitting이 일어나지 않았는지 확인했다.

저자들은 최종 검증을 한 번만 수행했었으며, 작년에는 결과를 두 번만 제출했다는 점을 강조하고 싶다고 함. 테스트의 수에 따라, 제안하는 모델의 일반적인 성능을 추정할 수 있다고 믿기 때문.

각 두 번의 결과는 각각 BN-Inception과 ILSVRC 2015 CLS-LOC 대회에 해당한다.


마지막으로, InceptionIncepion-ResNet의 다양한 버전에 대한 성능 비교를 보여준다.


Inception-v3Inception-v4는 residual connection을 활용하지 않는 deep convolutional network이며, Inception-ResNet-v1Inception-ResNet-v2는 filter concatenation 대신 residual connection을 이용하는 Inception style network이다.


Table.2는 validation set에 대한 다양한 구조들의 single model, single-crop 성능을 보여준다.


Table.2

Table.2
Single-crop, single model에 대한 실험 결과이다.

각 성능은 ILSVRC 2012의 validation set에서 non-blacklisted subset에 대해 측정됐다.


아래 Table.3 ~ Table.5의 각 crop 수는 ResNetGoogLeNet을 따른다.


Table.3은 다양한 모델들이 적은 수의 crop을 사용한 경우의 성능을 보여준다.
Table.3

Table.3
10/12-crop evaluation, single model에 대한 실험 결과이다.

각 성능은 ILSVRC 2012의 validation set인 50000개 이미지에 대해 측정됐다.


Table.4는 다양한 모델에 대한 multi-crop, single model 성능을 보여준다.


Table.4

Table.4
144-crop/dense evaluation, single model에 대한 실험 결과이다.

각 성능은 ILSVRC 2012의 validation set인 50000개 이미지에 대해 측정됐다.


Table.5는 ensemble 결과를 비교한다.


Table.5

Table.5
144-crop/dense evaluation, single model에 대한 실험 결과이다.

각 성능은 ILSVRC 2012의 validation set인 50000개 이미지에 대해 측정됐다.

Inception-v4(+Residual) ensemble은 Inception-v4 1개와 Inception-ResNet-v2 3개로 구성되며, validation set과 test set에 대해 평가됐다. 테스트 성능은 top-5 error가 3.08%이며, validation set에 overfitting되지 않았다.


6. Conclusions

이 논문에서는 3가지의 새로운 network architecture를 보였다.

  • Inception-ResNet-v1 : Inception-v3과 비슷한 계산 비용을 가진 하이브리드 버전

  • Inception-ResNet-v2 : Recognition 성능이 크게 향상 된 비싼 비용의 하이브리드 버전

  • Inception-v4 : Inception-ResNet-v2와 거의 동일한 recognition 성능을 가진 non-residual, pure Inception 버전


Residual connection의 도입으로 Inception 구조의 학습 속도가 얼마나 향상되는지 알아봤다. 또한, 제안하는 모델들은 모델의 크기가 커짐에 따라, residual connection의 유무에 상관없이 이들의 모든 이전 네트워크 성능을 능가했다.