[Classification] ResNet 코딩

2021. 6. 29. 17:10코딩연습장/Keras

이번에는 ResNet Network를 구축할 것이다. 

 

https://aistudy9314.tistory.com/29

 

[분류모델] ResNet

이번에는 기본 CNN에서 조금 구조적으로 진화된 Network를 살펴보려고한다. 지난번 VGGNet같은 경우, 단순 CNN에 3x3필터를 이용하였는데 ResNet은 Residual이라는 조금 새로운 모델 구조를 선보였다. 지금

aistudy9314.tistory.com

 

이제부터 VGGNet에서 사용한 Train code는 거의 건들지 않을 것이기 때문에, 따로 다루지 않고 모델 부분만 코딩할 것이다.

 


1. Layer

먼저 layer를 만들어보자. resnet은 보통 18, 34, 50, 101, 152의 size를 갖는다(논문 기준).

논문에서 50이상부터는 bottleneck block을 사용하고, 그 미만은 Normal Block을 사용한다.

따라서 그 두 block을 layer파일에 정의하였다.

 

또한, resnet v2에서 full-pre-activation 구조를 사용하기 위해 BN -> RELU -> CONV layer도 추가하였다.

'''Pre-activation'''
def bn_conv(inputs,filter_size, kernel_size, strides=1, padding="same", activation="relu", use_bias=False, regularizer=l2(0.001)):
    layer1 = keras.layers.BatchNormalization()(inputs)
    layer2 = keras.layers.Activation(activation=activation)(layer1)
    out = keras.layers.Conv2D(filters=filter_size, kernel_size=kernel_size, strides=strides, padding=padding, kernel_regularizer=regularizer, use_bias=use_bias)(layer2)
    return out

 

1-1. Normal Block

Normal Residual Block으로 inputs(X)은 변수에 저장해 놓고, 두 layer를 거친 output과 Add하게 된다.

여기서 strides=2일 경우, residual도 shape를 맞추어주어야 하기 때문에 Maxpooling layer를 한 번 거치고 난 후에 Add된다.

def residual_block(inputs, filter_size, kernel_size,  strides=1, padding="same", activation="relu", regularizer=l2(0.001), first_layer=False):
    preact = keras.layers.BatchNormalization()(inputs)
    preact = keras.layers.Activation(activation)(preact)

    if strides == 2:
        residual = keras.layers.MaxPooling2D(pool_size=1, strides=2)(inputs)
    else:
        residual = inputs

    block = keras.layers.Conv2D(filters=filter_size, kernel_size=kernel_size, strides=strides, padding=padding, kernel_regularizer=regularizer)(preact)
    block = bn_conv(block, filter_size=filter_size, kernel_size=kernel_size, strides=1, padding=padding, activation=activation, regularizer=regularizer)

    block_out = keras.layers.Add()([residual, block])
    return block_out

 

1-2. Bottleneck Block

Normal Block과 Flow는 같다. 다만, 1x1필터로 filter_size를 낮추었다가 원상태로 돌리는 것이 특징이다.

def residual_bottleneck_block(inputs, filter_size, kernel_size, strides=1, padding="same", activation="relu", regularizer=l2(0.001), first_layer=False):
    preact = keras.layers.BatchNormalization()(inputs)
    preact = keras.layers.Activation(activation)(preact)

    if first_layer:
        residual = keras.layers.Conv2D(filters=filter_size*4, kernel_size=(1,1), strides=1, padding=padding, kernel_regularizer=regularizer)(preact)
    elif strides == 2:
        residual = keras.layers.MaxPooling2D(pool_size=1, strides=2)(inputs)
    else:
        residual = inputs

    block = keras.layers.Conv2D(filters=filter_size, kernel_size=(1, 1), strides=1, padding=padding, kernel_regularizer=regularizer)(preact)
    block = bn_conv(block, filter_size=filter_size, kernel_size=kernel_size, strides=strides, padding=padding, activation=activation, regularizer=regularizer)
    block = bn_conv(block, filter_size=filter_size*4, kernel_size=(1, 1), strides=1, padding=padding, activation=activation, regularizer=regularizer)

    block_out = keras.layers.Add()([residual, block])
    return block_out

 

2. Model

논문에 나와있는 다음 표를 보고 개발을 진행하였다. 

출처: https://arxiv.org/pdf/1512.03385.pdf

2-1 __Init__

ResNet의 Config 값들을 정의 및 초기화하였다. 몇 변수를 간단히 설명하자면,

  • filter_list: layer당 filter size를 정의.
  • possible_resnet: support 하는 resnet의 크기들 설정.
  • layer_sizes: resnet 크기에 매칭되는 layer sizes를 정의해놓은 것. (ex. resnet50 -> [3, 4, 6, 3]).
  • block: layer num에 맞는 block정의.
    def __init__(self,input_shape, class_num, layer_num = 50, weight_decay=0.0001):
        self.input_shape = input_shape
        self.class_num = class_num
        self.l2_reg = keras.regularizers.l2(weight_decay)
        self.filter_list = [64, 128, 256, 512] # filter sizes
        possible_resnet =[18, 34, 50, 101, 152] # supported resnet layer

        # layer list for each resnet size
        layer_size_list = [[2, 2, 2, 2], [3, 4, 6, 3], [3, 4, 6, 3],
                           [3, 4, 23, 3], [3, 8, 36, 3]]

        resnet_num = possible_resnet.index(layer_num)
        self.layer_sizes = layer_size_list[resnet_num]

        if not layer_num in possible_resnet:
            print("Not Supported Resnet Size")
            print("Supported Size: 18, 34, 50 ,101, 152")
            exit(-1)

        if layer_num < 50:
            self.block = residual_block
        else:
            self.block = residual_bottleneck_block

 

2-2. Stem

약간 특이(?)한 stem을 가진다.

첫 Conv layer에 7x7 필터를 사용하고, Maxpooling에서도 3x3 pool size를 사용한다.

**pre-activation 구조로 인해 Batchnorm과 relu는 넣지 않는다.

    def resnet_stem(self, inputs):
        '''32 x 32'''
        block1 = keras.layers.Conv2D(filters=64, kernel_size=(7, 7), strides=2, padding="same", kernel_regularizer=self.l2_reg)(inputs)

        '''16 x 16'''
        block1_out = keras.layers.MaxPooling2D(pool_size=(3, 3), strides=(2, 2), padding="same")(block1)
        
        return block1_out

 

2-3. Body

이번에는 간단히 body를 만들어보았다. 더 customizing에 중점을 두고 싶다면, VGGNET처럼 만드는 것도 나쁘지 않다. Resnet은 반복되는 부분이 많아 반복문을 이용하여 짧게 만드는 것을 선호하였다.

    def resnet_body(self, x):
        block_size = len(self.layer_sizes)
        for s in range(block_size):
            for l in range(self.layer_sizes[s]):
                if l==0: # first layer
                    x = self.block(x, filter_size=self.filter_list[s], kernel_size=(3,3), strides=1, padding="same", activation="relu", regularizer=self.l2_reg, first_layer=True)
                elif s!=block_size-1 and l==self.layer_sizes[s]-1: # last layer except last block
                    x = self.block(x, filter_size=self.filter_list[s], kernel_size=(3,3), strides=2, padding="same", activation="relu", regularizer=self.l2_reg, first_layer=False)
                else:
                    x = self.block(x, filter_size=self.filter_list[s], kernel_size=(3,3), strides=1, padding="same", activation="relu", regularizer=self.l2_reg)
        return x

 

2-4 Resnet

Resnet에서는 VGGNet과 다르게 Flatten이 아닌 Global Average Pooling을 사용한다. 또한, 큰 연산량과 과도한 파라메터 개수를 유발했던 fully connected layer를 없애고, 최종 단에만 classification을 위해 사용하였다. 

    def resnet(self):
        inputs = keras.layers.Input(shape=self.input_shape)
        '''stem'''
        stem = self.resnet_stem(inputs)

        '''body'''
        body_out = self.resnet_body(stem)

        '''head'''
        head = keras.layers.BatchNormalization()(body_out)
        head = keras.layers.Activation("relu")(head)
        g_avg = keras.layers.GlobalAveragePooling2D()(head)
        fc_out = keras.layers.Dense(units=self.class_num, activation="softmax", use_bias=False)(g_avg)
        return keras.models.Model(inputs, fc_out)

 

3. Train

이제 train 시켜보자! VGGNET때와 같은 옵션을 주었고, 마찬가지로 Stem에서 stride 2부분과 maxpool layer를 제외해주었다. 

 

어느정도 시간이 지난 후에 VGGNET과 비교해보았다. 초반 수렴 속도는 Resnet이 압도적으로 빨랐지만 어느 시점 기준으로 점차 VGGNet과 비슷한 값으로 수렴하는 것을 볼 수 있다.

 

또한, validation accuracy도 마찬가지로 VGGNet의 결과와 별반 다를게 없다. 

 

그 이유를 잠깐 생각해보자. 개인적인 생각으로는 일단 데이터셋부터 잘못 정한 것 같다. CIFAR-100이 32x32의 shape를 가지는데, input shape가 너무 작아 네트워크가 제대로 학습하기 너무 힘들다. 

 

papers with code 사이트 기준으로 Resnet-1001이 77이 accuracy를 가진다 하니...50으로는 어림도 없을 것 같다. 

 

그리고 val accuracy부분에서 VGGNet이 Resnet보다 성능이 좋은 것처럼 보이는데, 그 이유는 Stem이라고 생각한다. 안그래도 input shape가 작은데 32x32 shape에서 VGGNet은 2개의 layer를 거치고, Resnet은 1개의 layer만 거친다. 만약 데이터셋이 더 복잡하고, 이미지 크기도 커진다면 Resnet이 더 성능이 좋을 것이라 생각한다.


Resnet을 만들어보았는데 결과가 썩 좋지 않았다. 다음 번에는 좀 더 큰 데이터셋을 가지고 훈련을 시켜볼 예정이다. 또한, 추후에 Learning Rate Scheduler나 Data Augmentation을 적용해보는 것도 생각 중이다.