9 minute read

Abstract

대규모 이미지 인식에서 Convolutional Network의 깊이와 정확도에 대해 연구했다. 상대적으로 작은 3x3 합성곱 필터를 사용하여 층을 깊게 만들었고, 16 - 19개의 층을 쌓았을 때의 모델이 기존에 연구되었던 모델들의 성능보다 더 좋은 성능을 기록했다. ImageNet Challenge 2014에서 localisation 부문 1등, classification 부문 2등을 차지했다. 본 연구에서 사용된 모델은 다른 데이터셋에서도 좋은 일반화 능력을 보였고, CV 분야에 도움이 되고자 가장 좋은 ConvNet 모델을 공개한다.



Details

도입부

  • ConvNet이 활발하게 사용되면서 AlexNet의 논문과 같이 성능을 높이려는 여러가지 시도들이 존재
  • 해당 논문에서는 네트워크의 깊이를 늘리는 것에 초점을 맞췄고, 깊이를 늘리는 대신 작은 3 x 3 convolution filter를 사용
  • 결과적으로 기존 모델들보다 성능이 좋은 모델을 얻었음



모델 구조와 분류

1. Architecture

  • 합성곱층의 깊이에 따라 성능이 달라지는 것을 확인하기 위해 기본적인 구조는 Ciresan의 2011년 논문과 Krizhevsky의 2012년 논문(AlexNet)을 참고
  • 224 x 224 크기의 RGB 이미지를 입력받고, 원본 픽셀값에서 평균을 뺀 전처리 이외의 전처리는 수행하지 않음
  • 주로 3 x 3 필터를 사용하고 16 weights layers인 network(Table 1. C의 경우)에서는 1 x 1 필터를 사용하기도 함(stride와 padding은 모두 1)
  • 합성곱층의 stride는 1로 고정, 특정 합성곱층 뒤에는 max pooling(2x2, stride 2)층 추가
  • Fully Connected 계층에서는 첫번째와 두번째 계층에서 4096개의 채널, 마지막 층에서는 1000개로 분류 진행
  • 모든 은닉계층에서 ReLU 사용
  • Local Response Normalization은 하나의 네트워크에서만 사용. 오히려 연산량과 메모리 소비량을 증가시키는 기법



3 x 3 filter

저자의 경우 이전 모델들이 11x11 혹은 7x7 필터를 사용한 것에 비해 3x3이라는 작은 필터를 사용했다. 3x3을 두겹으로 사용하면 5x5의 효과를, 세겹으로 사용하면 7x7의 효과를 낼 수 있다. 예를 들어 7x7이미지가 있을 때 5x5 필터를 사용하면,



\[\frac{7-5 + 2 \times 0}{1} + 1 = 3\]



3x3의 결과를 얻을 수 있고 3x3 필터를 두번 사용하면,



\[\frac{7-3 + 2 \times 0}{1} + 1 = 5\]


\[\frac{5-3 + 2 \times 0}{1} + 1 = 3\]



5x5 -> 3x3의 결과를 얻을 수 있다.

같은 receptive field 효과를 가진다면 굳이 5x5 필터를 한번 사용하지 않고 번거롭게 3x3 필터를 두번 사용하는 이유는 무엇일까? 예를 들어, 어떤 입력 이미지에 대해 채널의 수를 C, 3x3 필터를 2번 사용했다고 가정해보자. 그렇다면 이 층에서의 파라미터 수는 3x3x2xC^2이 될 것이고 만약, 5x5 필터를 한번 사용한다면 이 층에서의 파라미터 수는 5x5xC^2이 될 것이다(C가 제곱인 이유는 input과 output의 채널수를 동일하다고 가정했기때문. 편향은 편의상 제외).

결과적으로, 같은 receptive field 효과를 가지더라도 3x3 필터를 여러번 사용했을 때 연산량이 훨씬 줄어들기 때문에 3x3 필터를 여러겹 사용하는 것이 더 효과적인 방법이 된다. 또한, ReLU 함수를 여러층에서 통과하게됨으로써 비선형성이 증가해 모델의 학습에 더 도움을 준다.

1 x 1 filter

1x1 필터를 굳이 사용하는 이유는 입력과 출력의 채널 수가 같아서 receptive field에 영향을 주지 않는 선형 결합을 할 수 있으면서도 ReLU함수를 통과시켜 출력값에 비선형성을 추가할 수 있다는 장점을 가지고 있기 때문이다. 아래 Table 1의 configuration C에서 사용했다. 하지만, C보다 3x3 필터를 사용한 D의 성능이 더 높은 것을 보아 1x1 필터를 통해 비선형성을 추가하는 것도 중요하지만, 공간적인 정보를 잘 보존하는 것도 중요하다는 것을 알 수 있다. 1x1 필터는 주변 픽셀의 공간 정보를 보존하기 어렵다.



2. Configuration


image



위의 표는 합성곱층의 깊이를 달리해서 시도한 여러개의 네트워크들이다. A 네트워크는 8개의 합성곱층과 3개의 FC층, E는 16개의 합성곱층과 3개의 FC층으로 이루어져있다. 합성곱층의 채널의 갯수는 max pooling 이후 2의 제곱으로 증가한다. 각 네트워크마다 이전 네트워크에 비해 추가된 부분은 볼드로 표시했다. 예를 들어 A의 첫번째 합성곱층에는 LRN을 수행하지 않았는데 A-LRN에서는 LRN을 수행했기때문에 볼드로 표시했다.

또한, 아래 표는 각 네트워크별로 사용된 파라미터의 갯수를 나타낸 것이고 3x3 필터를 사용했기때문에 깊이가 깊어진 것에 비해서 사용된 파라미터의 수가 크게 증가하지 않았다는 것을 확인할 수 있다.

image



학습

VGG의 학습 과정은 학습 시에 다양한 크기의 입력 이미지들을 크롭하여 샘플링 하는 방법을 사용하는 것을 제외하면 AlexNet과 동일하고, 사용한 하이퍼파라미터는 다음과 같다.

  • Optimizer: 미니배치 경사하강법
  • Loss function: 다항 로지스틱 회귀
  • batch_size = 256, momentum = 0.9, weight_decay = L2, 0.0005
  • 1,2 번째 FC 계층에서의 dropout = 0.5
  • 초기 학습률 = 0.01(개선되지 않으면 10배 감소)

위의 하이퍼파라미터를 사용한 결과, 학습률이 총 3번 줄어들고 74에포크 동안 학습이 진행됐다. AlexNet에 비해 파라미터가 더 많고 깊이가 더 깊었는데도 작은 합성곱 필터크기와 regularization 등의 효과 덕분에 에포크가 많이 필요하지 않았다.

네트워크에서 가중치를 초기화 하는 것은 학습의 안정성 측면에서 매우 중요한데 저자는 깊이가 얕은 A 아키텍처를 랜덤으로 초기화한 가중치를 더 복잡한 아키텍처들의 1~4번째 convolutional 층과 마지막 3개의 FC층의 가중치를 초기화하는 용도로 사용했다. 랜덤 초기화는 가능하다면 평균이 0, 분산이 0.01인 정규분포로 초기화 했고 편향은 0으로 초기화했다. 여기서 중요한 점은 논문을 제출한 후에 알게 된 사실이지만 Glorot & Bengio의 2010년 논문에 따르면 사전훈련 없이 가중치를 초기화하는 방법이 존재했다.

학습에 사용할 224 x 224 사이즈의 이미지를 만들기 위해서는 다양한 크기의 input 이미지를 crop 해야한다. crop은 AlexNet과 같이 좌우반전과 RGB 색상변환을 사용했고 사이즈에 대해서는 3가지 방법을 통해 crop을 진행했다.

  • input 이미지를 256 x 256 사이즈로 고정(single-scale model)
  • input 이미지를 384 x 384 사이즈로 고정(single-scale model)
  • input 이미지를 [256, 512] 범위에서 랜덤하게 resize(multi-scale model)

위와 같은 방법을 사용하여 다양한 크기의 이미지가 입력되어도 학습이 잘 이루어지게 했고, 특히 3번째 방법의 경우 사전에 학습한 2번째 방법의 모든 레이어를 Fine tuning하여 학습시간을 줄였다.



검증

검증에서도 학습과 마찬가지로 고정된 크기의 입력 이미지를 사용했는데 검증 이미지의 크기가 꼭 학습 이미지의 크기와 같을 필요는 없고, 오히려 학습 이미지와 다른 크기의 이미지를 사용했을 때 성능이 더 좋아졌다. 검증 과정에서는 과적합을 방지하기 위해 기존의 FC계층을 convolutional 층(첫번째 FC층은 7x7, 두세번째 층은 1x1)으로 수정하여 기존에 Fully Connected Layer가 Fully Convolutional Layer라고 불리기도 한다. FC 계층을 통과한 출력값은 각 클래스에 속할 점수맵으로 이루어져있고 채널의 수는 예측하고자 하는 Class의 수와 동일하다.

검증 이미지에도 학습과 마찬가지로 데이터 증강을 적용하게되는데 원본 이미지와 증강 이미지에 대한 클래스들의 점수맵을 구한 후 soft-max에 평균을 취해 하나의 이미지에 대한 클래스별 점수맵을 얻는다. 최종적으로는 고정된 벡터를 얻기 위해 class score map에 spatially averaged를 취한다. Spatially averaged는 공간적으로 평균을 구한다는 의미로 한 위치에서 여러개의 클래스에 속할 확률을 평균화하여 하나의 클래스에 대한 확률값으로 변환시키는 것을 말한다.

Fully Convolutional Network를 사용하여 이미 이미지내의 전체 영역을 사용할 수 있기때문에 검증 과정에서 여러번 이루어지는 crop은 연산량의 측면에서 불필요해졌다.



GPU

Multi GPU를 사용하여 여러개의 이미지 배치를 각 GPU마다 할당했고 각각의 GPU에서 나온 기울기들을 평균 내어 전체 이미지 배치의 기울기를 구했다. 그 결과, 하나의 GPU를 사용했을 때와의 결과가 일치했다. 4개의 GPU를 사용했을 때 단일 GPU보다 약 3.75배 빠른 속도를 보였고 아키텍처에 따라 학습 시간이 2~3주 정도 소요됐다.



평가

Single Scale Evaluation

테스트 데이터에 대한 single scale evaluation에서는 학습 이미지를 256 or 384로 고정된 이미지를 사용하는 것보다 [256,512] 범위로 랜덤하게 resizing한 이미지를 사용하는 것이 더 성능이 좋았다. 이를 통해 scale jittering을 통한 학습 데이터 증강이 multi scale의 이미지 정보를 파악하기 좋다는 것을 알 수 있다. 성능은 아래의 표와 같이 층이 깊을수록 뛰어났고, A-LRN이 A보다 에러율이 높을 것을 보아 Local Reponse Normalization이 큰 도움이 되지 않다는 것을 확인할 수 있다.



image



Multi-Scale Evaluation

Multi scale evaluation에서는 다양한 스케일의 테스트 이미지를 평균내어 성능을 평가했다. 그 결과, 테스트 이미지에 대해 single scale evaluation과 같이 스케일을 고정시키는 것보다 scale jittering을 적용시켰을 때 성능이 더 좋았고 역시 층의 깊이가 깊을수록 에러율이 더 낮았다. Single scale evaluation보다 더 개선된 성능이다.



image



Multi-Crop Evaluation

Multi crop evaluation에서는 원본 이미지에 대한 dense evaluation과 증강 이미지에 대한 multi-crop evaluation의 soft-max output을 평균화해서 성능을 평가했다. Multi crop evaluation을 사용하면 다양한 크기의 패치를 네트워크에 적용시킨 후 네트워크마다의 분류 결과를 합쳐야해서 하나의 이미지지만 패치의 갯수만큼 연산이 여러번 증가한다. 하지만, dense evaluation은 이미지를 crop하는 것이 아니라 하나의 이미지를 1x1 필터와 같이 일정한 간격을 갖는 필터로 연산을 수행하여 분류기에서도 합성곱과 같은 sliding window 개념을 적용할 수 있다. 패치의 갯수만큼 여러가지 네트워크의 결과를 합치는 것보다 1x1 필터로 하나의 이미지에 대해 정밀하게(dense) 분류를 수행하는 것이 연산량의 측면에서도, 이미지의 공간 정보를 유지할 수 있는 측면에서도 더 좋은 방법이 됐다. 연산량의 측면에서는 dense evaluation이 더 좋지만, grid의 크기에 따라 성능이 나빠질 수 있기때문에 multi crop evaluation과 적절히 조화시켜 평균을 내는 방법을 사용했다. 결국, 이전 결과들과 마찬가지로 층이 깊을수록 작은 에러율을 보였다.



image


그 이후 여러가지 모델을 조합하여 성능을 향상 시켰고, 최종적으로는 아래의 표와 같이 ILSVRC classification에서 2등으로 입상했고, Single Net 관점에서는 GoogLeNet보다 0.9% 낮은 에러율을 보였다.



image



결론

층의 깊이가 깊을수록 분류의 정확도에 긍정적인 영향을 끼치는 것을 확인했고, 기존 Convolutional Network 아키텍처의 큰 변화없이 깊이만 증가시켜 좋은 성능을 낼 수있음을 보였다.



개인적인 생각

  • 저자가 언급했듯이 기존에 고안된 Convolutional Network의 기본 구조를 변형시키지 않으면서 단순히 깊이를 더해 성능이 좋은 모델을 만들었다는 점에서 굉장히 의미있었던 연구였던 것 같다.
  • AlexNet에서는 filter의 크기, stride와 같은 하이퍼파라미터를 실험을 통해 찾아냈지만, VGG에서는 대부분 3x3 필터로 크기를 통일해서 사용했다. 실험시간을 고려하면 굉장히 단순하지만 효율적인 발상이다.
  • AlexNet에서 ReLU의 출력값을 적절히 조정하기 위해 사용된 LRN이 VGG에서는 오히려 효과가 좋지 못했다. 정규화를 시키는 것이 무조건적으로 좋은건 아니라는 근거가 될 수 있다.
  • 학습이든, 검증이든 고정된 이미지의 크기가 아닌 다양한 이미지의 크기를 사용했을때 성능이 더 높았다. ‘이미지 사이즈 조절’이라는 데이터 증강에도 single scale, multi scale 처럼 여러가지 방법이 존재한다.
  • 검증 단계에서 과대적합을 방지하기 위해 기존 완전결합층을 합성곱층으로 변환시켰는데 이후에 나온 다른 모델들은 어떠한 방법을 사용하여 이를 해결했는지 궁금증이 생겼다. 만약 완전결합층을 바꾸지 않으면서 과적합이 발생하지 않게 하는 방법을 어떻게 사용할까?
  • 1 x 1 필터를 사용한 연산을 굳이 왜 사용하나라는 생각이 들었었는데 본 논문을 읽으며 필요성을 깨달았다. 사소하다고 생각한 작은 필터로부터 성능 향상을 이뤄낼 수도 있다.



구현

Pytorch로 VGGNet을 구현해보자(참고)

# 필요 함수 정의
# 합성곱층이 여러번 반복되기 때문에 가독성을 위해 미리 정의
def conv_2_blocks(in_chn, out_chn):
    conv_net = [
            #합성곱
            # 첫번째층                    
            nn.Conv2d(in_channels = in_chn, out_channels = out_chn, kernel_size = 3, 
                    stride = 1, padding = 1),
            
            # batch 별로 평균과 분산을 이용한 정규화. 
            # affine: scale과 shift를 학습 시킴. scale은 정규화된 출력 조정, shift는 출력 이동, False하면 단순 정규화만
            # track_running_stats: True로 하면 배치별로 이동 평균과 이동 분산 계산. 이는 정규화된 출력을 계산할 대 사용. False하면 배치별로만 계산
            nn.BatchNorm2d(out_chn, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True),
            nn.ReLU(inplace=True), # True로 설정해서 입력 텐서를 계속 수정하면서 매모리 절약. 이전의 입력 텐서 정보가 저장되지 않음.
            
            # 두번째층의 input 채널 수는 첫번째층의 output 채널 수와 같다.
            nn.Conv2d(in_channels = out_chn, out_channels = out_chn, kernel_size = 3, 
                    stride = 1, padding = 1),
            nn.BatchNorm2d(out_chn, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True),
            nn.ReLU(inplace=True),        
            nn.MaxPool2d(kernel_size=2, stride=2)
    ]
    
    return conv_net 

def conv_4_blocks(in_chn, out_chn):
    conv_net = [
            # 첫번째층
            nn.Conv2d(in_channels = in_chn, out_channels = out_chn, kernel_size = 3, 
                    stride = 1, padding = 1),
            nn.BatchNorm2d(out_chn, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True),
            nn.ReLU(inplace=True),        
            
            # 두번째층
            nn.Conv2d(in_channels = out_chn, out_channels = out_chn, kernel_size = 3, 
                    stride = 1, padding = 1),
            nn.BatchNorm2d(out_chn, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True),
            nn.ReLU(inplace=True),
                    
            # 세번째층
            nn.Conv2d(in_channels = out_chn, out_channels = out_chn, kernel_size = 3, 
                stride = 1, padding = 1),
            nn.BatchNorm2d(out_chn, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True),
            nn.ReLU(inplace=True),        
            
            # 네번째층
            nn.Conv2d(in_channels = out_chn, out_channels = out_chn, kernel_size = 3, 
                    stride = 1, padding = 1),
            nn.BatchNorm2d(out_chn, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True),
            nn.ReLU(inplace=True),        
            nn.MaxPool2d(kernel_size=2, stride=2)
    ]
    
    return conv_net       
        
class VGG(nn.Module):
    # 모델 구조
    def __init__(self, num_classes=1000, init_weights=True):
        # nn.Module 클래스의 init을 호춣할 때 VGG 클래스와 현재 객체 정보를 전달 
        super(VGG, self).__init__()
        
        layers = []
        layers += conv_2_blocks(in_chn=3, out_chn=64) # 112 x 112 x 64, 1~2 층
        layers += conv_2_blocks(in_chn=64, out_chn=128) # 56 x 56 x 128, 2~4 층
        layers += conv_4_blocks(in_chn=128, out_chn=256) # 28 x 28 x 256, 5~8 층
        layers += conv_4_blocks(in_chn=256, out_chn=512) # 14 x 14 x 256, 9~12 층  
        layers += conv_4_blocks(in_chn=512, out_chn=512) # 7 x 7 x 512, 13~16 층  
        self.net = nn.Sequential(*layers)
        
        self.classifier = nn.Sequential(
            #17층
            nn.Linear(in_features=7 * 7 * 512, out_features=4096),
            nn.ReLU(),
            
            #18층
            nn.Dropout(0.5),
            nn.Linear(in_features=4096, out_features=4096),
            nn.ReLU(),
         
            #19층
            nn.Dropout(0.5),
            nn.Linear(in_features=4096, out_features = num_classes)
        )
        
        if init_weights:
            self._initialize_weights()
        
    # 순전파
    def forward(self, x):
        x = self.net(x)
        x = torch.flatten(x, 1) # 7 x 7 x 512 크기를 1차원으로 축소
        x = self.classifier(x)
        return x
    
    def _initialize_weights(self):
        for m in self.modules():
            if isinstance(m, nn.Conv2d): # 합성곱층의 네트워크면
                # relu를 사용했기 때문에 He 초기화
                # fan_out: 정규 분포의 분산을 결정할 때 출력 채널 수를 기준으로 분산 조절. fan_in은 입력 채널.
                # 가중치 초기화 잘 이루어져야 학습이 안정적으로 이루어질 수 있다.
                nn.init.kaiming_normal_(m.weight, mode = 'fan_out', nonlinearity='relu')
                if m.bias is not None: # 편향이 존재하면
                    nn.init.constant_(m.bias, 0) # 0으로 초기화
            elif isinstance(m, nn.BatchNorm2d): # 배치 정규화는
                nn.init.constant_(m.weight, 1) # 가중치는 1로
                nn.init.constant_(m.bias, 0)# 편향은 0으로
            elif isinstance(m, nn.Linear): # FC층에서는
                nn.init.normal_(m.weight, 0, 0.01) # 가중치를 평균 0, 분산이 0.01인 정규분포로
                nn.init.constant_(m.bias, 0)# 편향은 0으로 초기화

이미지 출처

Categories:

Updated: