매직코드
article thumbnail

논문을 선정한 이유

  • 앞으로 회사 업무에서 CV 파트도 맡게 되었기 때문에 가장 범용성 높은 방법론에 대해 공부하기 위함
  • AlexNet보다 실용성있고 코드를 구현해본다면 앞으로도 계속 유용하게 사용할 모델이 될 것 같음
  • 이미지 분석에 큰 영향을 끼친만큼 많은 리뷰들과 코드들이 있기에 첫 논문리뷰를 진행할 때 참고할만한 자료가 많음

논문 읽기

Abstract

밑줄 친 문장 중 가장 진하게 표시된 부분이 이 논문을 대표하는 문장이라고 생각된다.

"우리는 잔차학습 프레임워크를 제안한다. 이전에 사용된 것보다 깊은 네트워크의 학습을 쉽게 하기 위해서."

이를 뒷받침하고자 residual(잔차)를 이용한 모델이 ImageNet test set에서 3.75%의 에러를 달성했다는 내용과 LISVRC & COCO 2015 competition에서 좋은 성적을 거두었고, ImageNet detection, ImageNet localization, COCO detection, COCO segmentation에서도 좋은 성능을 보였음을 설명한다.

 

Introduction

<요약>

  • 더 나은 networks를 학습하는 것이 layer를 더 많이 stacking 하는 것만큼 쉬운가?
  • Convergence Problem이 있지만 SGD로 해결 가능
  • Degradation Problem은 과적합때문이 아니라 layer가 많아질수록 학습이 잘 안되는 문제임
  • Degradation을 해결하기 위한 방법으로 잔차학습을 제안함

 

이 논문에서는 Figure 1.을 통해 기존의 심층모델의 문제점을 설명한다. 그래프를 보면 왼쪽이 학습데이터 오류이고, 오른쪽이 테스트데이터 오류를 나타낸다. 20layer을 사용한 더 얕은 모델이 56layer를 사용한 깊은 모델보다 오류율이 더 작게 나타나는 것을 확인할 수 있다. 이 논문에서 찝은 문제점은 더 깊은 모델일수록 오류율이 더 낮게 나와야하는데 그렇지 않다는 점이다.

깊은 모델에서 오류가 더 높게 나오는 이유에 대해서는 과적합이 원인이 아니라 훈련데이터셋부터 깊은모델이 학습을 잘 하지 못한다고 주장했다. 위의 Figure 1. 에서 왼쪽그래프를 보면 훈련데이터 역시 과적합이 아니라 깊은 모델이 학습이 잘 되지 않는 것을 확인할 수 있다. 훈련이 잘 되지 않았기 때문에 테스트데이터에서도 깊은 모델의 오류율이 높다고 설명한다. (과적합이라고 보려면 왼쪽의 train 그래프의 오류율은 iter가 진행될수록 0과 더 가까워져야하고 오른쪽의 test 그래프의 오류율은 iter가 진행될수록 발산하는 양상을 보여야한다.)

이 논문에서는 심층 잔류 학습 프레임워크(이하 ResNet)를 도입하여 성능저하 문제를 해결하고자 한다.

여기서 mapping이라는 단어가 나오는데 나는 이것을 input과 output세트를 의미한다고 이해했다. 기존의 매핑은 input-layer-output으로 레이어 하나마다 매핑이 하나씩 있는데 이 논문에서는 input-layer-layer-output과 같이 여러개의 layer를 묶어 block으로 만들고 block마다 하나의 매핑이 있도록 모델을 구성했다.

이 논문에서는 input x 가 layer를 하나 지나면 output으로 H(x)를 나타내는 기존매핑 대신에 output인 H(x)에서 x를 뺀 값 즉, 잔차를 사용하려고 한다. 이 때 output은 F(x) = H(x) - x 로 표현할 수 있다. 이를 다시 쓰면 H(x) = F(x) + x 다.

Figure 2.에서 볼 수 있듯이 input은 x이고 layer를 하나 지나면 F(x), layer를 하나 더 지나면 output = F(x) + x 의 형태다. 잔차로 나오는 F(x)에 input x값을 더해서 기존매핑에서 나오는 H(x)와 동일한 형태가 된다.

참조되지 않은 매핑을 최적화하는 것보다 잔류 매핑을 최적화 하는 것이 더 쉽다고 가정하고, 아이덴티티 매핑(input값과 output값이 동일하게 나오는 매핑)을 맞추는 것보다 잔차를 0으로 만드는 것이 더 쉬울 것이라는 아이디어에서 잔차매핑이 나온 것 같다.

논문은 2가지를 보여주고자 한다. We show that 부터 보면 1) 최적화가 더 잘 된다. 2) accuracy가 더 높게 나온다.

결과론적인 풀이이지만 Abstract에서 썼던 것 처럼 여러 방면에서 뛰어난 성능을 보여주었기에 논문이 보여주고자 하는 것은 다 보여준 것 같다.

Related Work

이 부분은 ResNet에 사용된 개념들이 다른 모델에서는 어떻게 사용되는지 설명하는 부분이기 때문에 넘어간다.

 

Deep Residual Learning

Residual Learning

잔차학습은 Introduction에 나왔던 것 처럼 기존의 H(x)중심 계산식에서 F(x)중심 계산식으로 생각을 바꾸는 느낌이다.

기존의 비선형함수가 H(x)를 근사할 수 있다고 가정한 상황이었다면 잔차인 F(x)도 당연히 근사할 수 있다고 가정했다.

학습 할 때 이전 레이어에서 output으로 나온 H(x)를 학습하면서 근사한다면 값이 큰 H(x)를 학습하는 것보다 값이 작은 잔차F(x)를 학습하는 것이 조금 더 쉽다는 것을 증명했다.

그래프의 위쪽은 일반적인 모델학습의 결과이고 아래쪽은 잔차학습을 적용한 경우인데, 위쪽 그래프를 보면 각 레이어를 지날 때마다 std의 값이 들쭉날쭉 해지는 것을 볼 수 있다. 반면에 아래쪽 그래프를 보면 비교적 합리적인 결과를 나타낸다.

 

Identity Mapping by Shortcuts

이 논문에서 Identity Mapping과 Shortcuts는 모두 결과값에 입력값 x를 더한다는 의미를 가진다. 

계산식에서 볼 수 있듯이 bias는 고려하지 않도록 정의했다. 기본적으로 1번식을 사용하는데 input과 output의 차원이 일치하지 않는 경우 Ws를 통해서 차원을 동일하게 맞춰주는 2번식을 사용했다.

 

Network Architectures

이 부분에서는 VGG-19 와 잔차학습을 하지 않은 34-layer plain, 잔차학습을 한 34-layer residual을 비교한다.

34-layer plain은 VGG의 모형을 비슷하게 사용하고자 3x3 filters 를 사용한다. output feature map size를 동일하게 만들기 위해서 layer들은 동일한 개수의 filter를 사용하고, feature map size가 반이되면 filter 개수를 두배로 늘려서 layer마다 time complexity를 보존하게 만들었다. conv layer의 stride를 2로 만들어서 직접적으로 다운샘플링 되도록 만들었다.

 

34-layer residual은 plain 모델에서 residual learning(잔차학습)만 적용한 것을 볼 수 있다. 2개의 conv마다 잔차학습을 수행한 것을 알 수 있고, 화살표가 점선으로 이루어진 것은 입력층과 출력층의 demension 차이가 있어서 그 부분에 대한 추가적인 처리가 있는 2번식을 사용했다는 것을 표현한다.

Residual Network(ResNet)은 input과 output의 차원이 동일하면 identity shortcuts(1번식)을 바로 사용할 수 있다. 차원이 동일하지 않다면 0으로 이루어진 패딩을 추가해서 1번식을 사용하거나, 파라미터를 추가 할 필요 없이 2번식을 이용하는 방법이 있다.

 

Implementation

학습을 어떻게 진행했는지 설명하는 부분이다. 기본적으로 ImageNet을 사용한 논문 Imagenet classification with deep convolutional neural networksVery deep convolutional networks for large-scale image recognition에서 구현한 모델을 따랐다.

  • 224x224로 잘려진 이미지는 원본이미지 또는 수평으로 뒤집어진 이미지에서 랜덤하게 샘플링된다. 
  • conv layer 직후 batch normalization를 수행하고 활성화 한다.
  • 가중치를 초기화하고 모든 plain모델, residual모델을 처음부터 train한다.
  • mini-batch가 256인 SGD기법을 사용한다.
  • learning_rates : 0.1로 시작해서 오차가 줄지 않거나 60만번 반복되는 경우 10배 작게 설정한다.
  • 하이퍼파라미터인 weight decay는 0.0001으로 momentum는 0.9로 설정했다.
  • Test에는 표준 10개의 crop을 사용한다.

Experiments

이미지넷 분류, CIFAR-10, Object Detection과 segmentation에서 ResNet의 성능이 얼마나 좋았는지 실험한 결과를 알려주는 부분이여서 자세히 리뷰하지는 않고 ImageNet Classification 내용만 잠깐 보고 넘어간다.

 

ImageNet Classification

CIFAR-10 and Analysis

Object Detection on PASCAL and MS COCO

 

ImageNet 2012 128만개의 train, 5만개의 validation, 10만개의 test 데이터셋, 1000개의 class를 이용하여 평가를 진행했다.

  • plain에서 오류률을 34-layer가 18-layer보다 높다.
  • ResNet에서는 34-layer의 오류률이 18-layer보다 낮다.
  • 18-layer만 비교하면 ResNet의 오류률이 더 빨리 떨어졌다.

Identity와 Projection Shortcuts을 비교한 결과를 설명한다.

A : zero-padding을 이용해 차원 증가

B : 차원이 증가할 때만 projection shortcuts 사용

C : 모든 shortcuts에 대해서 projection 수행

  • C가 가장 좋은 성능을 보임
  • 하지만 A, B, C 사이에는 작은 성능차이만 있어 projection shortcuts이 성능저하를 해결하는 데 필수적 요소는 아님
  • Identity shortcuts는 bottleneck architectures에서 복잡도를 증가시키지 않게하기 위해서 중요함

 

 

논문 리뷰

저자가 뭘 해내고 싶어했는가?

깊은 네트워크를 학습시키기 위해서 잔차를 이용하여 학습하는 방법을 제안한다.

 

이 연구의 접근에서 중요한 요소는 무엇인가?

  • 학습을 더 쉽게 만드는 방법으로 잔차를 사용하는 것이다.
  • 다음 레이어에서 학습해야하는 결과값 H(x)가 아니라 결과값 H(x) - 실제값 x 를 계산한 잔차 F(x)를 학습할 수 있게 한다.
  • 이로인해 깊은 네트워크에서의 오류률을 줄어들고 오버피팅이 되지 않는 경우에 많이 학습할수록 정확도가 높아지는 결과를 보였다.

논문을 보고 느낀점?

  • 이미지모델에 사용되는 기본적인 용어 convolution, crop, mapping등을 미리 알고 논문을 리뷰하면 좋았을 것 같다.
  • 수식으로만 놓고 보면 H(x) = F(x) + x 인 것과 F(x) = H(x) - x 인것이 동일한 내용인데 모델에 어떻게 반영하느냐에 따라 성능차이가 많이 나는 것이 신기했다.

어떤 프로젝트에 적용할 수 있는가?

  • 이미지 분류에 범용적으로 사용할 수 있을 것 같다.
  • 특히 깊은 네트워크도 잘 수행하는 모델인만큼 복잡하고 여러운 이미지 분류에도 적합할 것 같다.

추가적으로 공부해야할 것 또는 참고하고 싶은 다른 레페런스에는 어떤 것이 있는가?

ImageNet의 기본과 ResNet을 사용한 다양한 활용 논문을, 대회수상코드 등을 보고 공부하고 싶다.

코드를 작성하다보니 dilation에 대한 이해가 부족해서 그 부분을 조금 더 공부해야겠다.

resnet이 이미지분류모델 뿐만 아니라 object detection이나 segmentation에도 좋은 성능을 보였다고 하니 그와 관련된 논문을 찾아볼 것 같다.

 

코드 구현

코드로 ResNet을 구현하기 전에 논문에서 resnet을 어떻게 만들었는지 확인해본다.

 

<18-layer>

  • 첫번째 층 conv1
    input = 3 (위 표에서는 안나와있지만 이미지 모델이기 때문에 RGB 3채널로 받는다)
    output = 64, kernel_size = 7x7, stride = 2, padding = 3x3
    또한 표에는 나와있지 않지만 BatchNorm2d와 ReLU, MaxPool2d를 수행한다.

  • 두번째 층 conv2
    여기부터는 block이 사용된다. 18-layer는 conv2에서 [[3x3, 64][3x3, 64]] x2 의 형태를 가진다. 이 때 [[3x3, 64][3x3, 64]] 이 부분을 block이라고 하고 크기에 변동이 없기 때문에 BasicBlock이라고 부른다. 두번째 층 conv2에는 BasicBlock이 2개 있다.

    BasicBlock 안에는 convolution layer가 2개 들어있다.
    input = 64, output = 64, kernel_size = 3x3, stride = 1, padding = 1, bias=False 와 같은 형식으로 layer가 만들어지고 자세한 사항은 아래 코드를 보면 알 수 있다.
  • 세번째 층 conv3, 네번째 층 conv4, 다섯번째 층 conv5
    이 부분도 input, output이 달라지는 것 말고는 두번째 층 conv2와 달라지는 것이 없다.
  • 마지막층
    다섯번재 층 conv5까지 수행하면 최종 output을 내보내기 전에 average pool과 softmax를 사용한다고 써있다.

<50-layer>

모델이 깊어짐에 따라 내부 구조가 살짝 변한다.

첫번째 층 conv1 은 동일하고, 두번째 층인 conv2 부터는 BasicBlock이 아니라 BottleneckBlock을 사용한다.

 

  • 두번째 층 conv2
    두번째 층 conv2에는 BottleneckBlock이 3개 있다.
    BottleneckBlock 안에는 convolution layer가 3개 들어있다.
    convolution1) input = 64, output = 64, kernel_size = 1x1, stride = 1, bias = False
    convolution2) input = 64, output = 64, kernel_size = 3x3, stride = 1, padding = 1, bias = False
    convolution3) input = 64, output = 256, kernel_size = 1x1, stride = 1, bias = False
    그 외 자세한 구조는 아래 코드로 확인할 수 있다.
  • 세번째 층 conv3
    세번째 층 conv3에는 BottleneckBlock이 4개 있다.
    두번째 층 conv2에서 output으로 나온 256이 128로 줄어들었다가 128의 4배(코드에서는 expansion으로 표현)인 512로 늘어난다.
  • 네번째 층 conv4
    네번째 층 conv4에는 BottleneckBlock이 6개 있다.
    세번째 층 conv3에서 output으로 나온 512가 256로 줄어들었다가 256의 4배인 1024로 늘어난다.
  • 다섯번째 층 conv5
    다섯번째 층 conv5에는 BottleneckBlock이 3개 있다.
    네번째 층 conv4에서 output으로 나온 1024가 512로 줄어들었다가 512의 4배인 2048로 늘어난다.
  • 마지막층
    마지막층은 Block의 종류와 상관없이 수행되는 부분이라 18-layer와 동일하게 average pool과 softmax를 사용한다.

 

본격적으로 코드를 구현해보자.

먼저 가장 많이 사용되는 convolution layer를 정의한다.

# convolution layer 정의
def conv3x3(in_planes, out_planes, stride=1, groups=1, dilation=1):
    return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride, padding=dilation, groups=groups, bias=False, dilation=dilation)

def conv1x1(in_planes, out_planes, stride=1):
    return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)

 

다음으로는 Block의 구조를 잡아준다.

얕은 모델에서는 BasicBlock을 사용하고 깊은 모델에서는 BottleneckBlock을 사용하니 두가지 Block을 모두 정의한다.

# Basic Block
class BasicBlock(nn.Module):
    expansion = 1
    
    def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1, base_width=64, dilation=1, norm_layer=None):
        super(BasicBlock, self).__init__()
        if norm_layer is None:
            norm_layer = nn.BatchNorm2d
        
        # 에러문구
        if groups != 1 or base_width != 64:
            raise ValueError('BasicBlock only supports groups=1 and base_width=64')
        if dilation > 1:
            raise NotImplementedError('Dilation > 1 not supported in BasicBlock')
            
        # 모델
        self.conv1 = conv3x3(inplanes, planes, stride) # resnet은 차원이 바뀌는 블록의 첫번째 conv에서 stride를 이용해 다운샘플링 한다.
        self.bn1 = norm_layer(planes)
        self.relu = nn.ReLU(inplace=True)
        self.con2 = conv3x3(planes, planes)
        self.bn2 = norm_layer(planes)
        self.downsample = downsample
        self.stride = stride
        
    def forward(self, x):
        indentity = x
        
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)
        
        out = self.conv2(out)
        out = self.bn2(out)
        
        # short connection
        if self.downsample is not None:
            identity = self.downsample(x)
            
        out += identity # 마지막 relu 전에 identity mapping 해주기!
        out = self.relu(out)
        
        return out
class Bottleneck(nn.Module):
    expansion = 4
    
    def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1, base_width=64, dilation=1, norm_layer=None):
        super(Bottleneck, self).__init__()
        
        if norm_layer is None:
            norm_layer = nn.BatchNorm2d
            
        # WideResNet, ResNeXt에서 사용
        width = int(planes * (base_width / 64.)) * groups
        
        # 모델
        # stride가 1이 아닌 경우, self.conv2, self.downsample 층 둘 다 input을 다운샘플한다.
        self.conv1 = conv1x1(inplanes, width)
        self.bn1 = norm_layer(width)
        
        self.conv2 = conv3x3(width, width, stride, groups, dilation)
        self.bn2 = norm_layer(width)
        
        self.conv3 = conv1x1(width, planes * self.expansion)
        self.bn3 = norm_layer(planes * self.expansion)
        
        self.relu = nn.ReLU(inplace=True)
        self.downsample = downsample
        self.stride = stride
        
    def forward(self, x):
        identity = x
        
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)
        
        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu(out)
        
        out = self.conv3(out)
        out = self.bn3(out)
        
        if self.downsample is not None:
            identitiy = self.downsample(x)
            
        out += identity
        out = self.relu(out)
        
        return out

 

Block들을 정의해줬으면 이제 resnet모델의 구조를 잡아주면서 정의한다.

 

 

 

class ResNet(nn.Module):
    def __init__(self, block, layers, num_classes=1000, zero_init_residual=False, groups=1, width_per_group=64,
                 replace_stride_with_dilation=None, norm_layer=None):
        
        super(ResNet, self).__init__()
        
        # 파라미터 기본설정 ========================
        if norm_layer is None:
            norm_layer = nn.BatchNorm2d
        self.norm_layer = norm_layer
        self.inplanes = 64 # input 기본값
        self.dilation = 1
        
        # 튜플의 각 요소는 2x2stride 대신 dilated convolution으로 대체해야하는지 나타낸다 : stride를 dilation으로 대체할꺼냐? 
        if replace_stride_with_dilation is None:
            replace_stride_with_dilation = [False, False, False]
        if len(replace_stride_with_dilation) != 3:
            raise ValueError(f'replace_stride_with_dilation should be None or a 3 element tuple, got {replace_stride_with_dilation}')
            
        self.groups = groups
        self.base_width = width_per_group
        # =======================================
        
        # 모델 ===================================
        # 모든 resnet 공통부분, RGB이미지이지 때문에 input=3, 공식문서를 참고하여 kernel_size와 padding 설정
        self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=7, stride=2, padding=3, bias=False)
        self.bn1 = norm_layer(self.inplanes)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        
        # layer1~4에서 block의 종류와 갯수로 resenet18, resnet50 등 모델의 차이가 생긴다.
        # 파라미터 layers[0]등은 아래 _make_layer에서 blocks로 받은 값 : block의 개수
        self.layer1 = self._make_layer(block, 64, layers[0])
        self.layer2 = self._make_layer(block, 128, layers[1], stride=2, dilate=replace_stride_with_dilation[0])
        self.layer3 = self._make_layer(block, 256, layers[2], stride=2, dilate=replace_stride_with_dilation[1])
        self.layer4 = self._make_layer(block, 512, layers[3], stride=2, dilate=replace_stride_with_dilation[2])
        
        # 모든 resnet 공통부분, 모든 block이 끝나면 수행
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.fc = nn.Linear(512 * block.expansion, num_classes)
        # =======================================
        
        # 모델 초기화
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
            elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
                nn.init.constant_(m.weight, 1)
                nn.init.constant_(m.bias, 0)
                
        # 각 residual branch의 마지막 BN에 zero-initialize를 해주어 residual branch는 zero로 시작하여 각 residual block이 identity처럼 작동하도록 함
        if zero_init_residual:
            for m in self.moduels():
                if isinstance(m, Bottleneck):
                    nn.init.constant_(m.bn3.weight, 0)
                elif isinstance(m, BasicBlock):
                    nn.init.constant_(m.bn2.weight, 0)
        
        # block 내부에 layer를 만드는 함수
        # block : 블록종류(basic, bottleneck)
        # blocks : 블록 내 layer 개수 (이 파라미터는 resnet18의 경우 [2, 2, 2, 2]로 입력받음) 
        def _make_layer(self, block, planes, blocks, stride=1, dilate=False):
            norm_layer = self._norm_layer
            downsample = None
            previous_dilation = self.dilation
            
            if dilation:
                self.dilation *= stride
                stride = 1
            
            # strid가 1이 아니거나 입출력차원이 맞지 않는경우 downsample 진행
            if stride != 1 or self.inplanes != planes*block.expansion:
                downsample = nn.Sequential(conv1x1(self.inplanes, planes*block.expansion, stride),
                                           norm_layer(planes*block.expansion),
                                          )
            
            layers = []
            layers.append(block(self.inplanes, planes, stride, downsample, self.groups, self.base_width, previous_dilation, norm_layer))
            self.inplanes = planes * block.expansion
            
            # 입력된 blocks만큼 block반복
            for _ in range(1, blocks):
                layers.append(block(self.inplanes, planes, groups=self.groups, base_width=self.base_width, dilation=self.dilation, norm_layer=norm_layer))
            
            return nn.Sequential(*layers)
            
        def _forward_impl(self, x):
            # See note [TorchScript super()] : 왜 써있는지 모르겠음...
            x = self.conv1(x)
            x = self.bn1(x)
            x = self.relu(x)
            x = self.maxpool(x)
            
            x = self.layer1(x)
            x = self.layer2(x)
            x = self.layer3(x)
            x = self.layer4(x)
            
            x = self.avgpool(x)
            x = torch.flatten(x, 1)
            x = self.fc(x)
            return x
        
        def forward(self, x):
            return self._forward_impl(x)
        
        # resnet18, resnet50 만들기
        def _resnet(arch, block, layers, pretrained, progress, **kwargs):
            model = ResNet(block, layers, **kwargs)
            if pretrained:
                state_dict = load_state_dict_from_url(model_urls[arch], progress=progress)
                model.load_state_dict(state_dict)
            return model
        
        def resnet18(pretrained=False, progress=True, **kwargs):
            return _resnet('resnet18', BasicBlock, [2, 2, 2, 2], pretrained, progress, **kwargs)
        
        def resnet50(pretrained=False, progress=True, **kwargs):
            return _resnet('resnet50', Bottleneck, [3, 4, 6, 3], pretrained, progress, **kwargs)

 

 

ResNet 실습 - ImageNet

위에서 만든 reset18 또는 reset50은 pretrained가 기본적으로 False로 되어있다. 실습에서는 보통 pretrained=True로 두고 모델을 가져와서 사용한다.

 

간단하게 reset18 모델을 가져와서 구조를 보면 위에서 만든 구조가 바로 보인다.

# pretrained 모델 불러오기
import torch
model = torch.hub.load('pytorch/vision:v0.10.0', 'resnet18', pretrained=True)
model.eval()

 

웹사이트에서 간단한 이미지를 하나 가져와서 pretrained모델이 이미지 분류를 잘 하는지 확인해보려고 한다.

랜덤하게 구글에서 금붕어 이미지를 가져왔다.

from urllib.request import urlretrieve
from PIL import Image

# url, filename = ("이미지주소.jpg", "임의로 정한 이미지 이름.jpg")
url, filename = ("https://upload.wikimedia.org/wikipedia/commons/thumb/b/b9/%E3%83%AF%E3%82%AD%E3%83%B320120701.JPG/500px-%E3%83%AF%E3%82%AD%E3%83%B320120701.JPG", "goldfish.jpg")
urlretrieve(url, filename)

# 이미지 확인
input_image = Image.open(filename)
input_image

 

이미지 정보를 tensor로 변경해준다.

# 이미지를 tensor로
preprocess = transforms.Compose([transforms.Resize(256),
                                 transforms.CenterCrop(224),
                                 transforms.ToTensor(),
                                 transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
                                ])

input_tensor = preprocess(input_image)
print(input_tensor.shape)

# 모델이 요구하는대로 mini-batch 만들어주기
input_batch = input_tensor.unsqueeze(0)
print(input_batch.shape)

 

이제 ImageNet의 정보를 가져온다.

ImageNet의 정보는 정답보기가 1000개가 있다고 생각하면 된다.

# imagenet의 클래스 정보 가져오기
imagenet_json, _ = urlretrieve('http://www.anishathalye.com/media/2017/07/25/imagenet.json')

with open(imagenet_json) as f:
    imagnet_labels = json.load(f)
print(imagnet_labels[1])
# imagenet에 들어있는 라벨은 1000개인데 그 중에 goldfish라는 라벨이 있다.

 

이제 pretrained 모델한테 질문을 할 차례다.

"이 사진은 무엇일까요? 1번 돌맹이, 2번 금붕어,  3번 강아지....1000번 컴퓨터" 와 같은 질문을 한다.

# 모델 학습
with torch.no_grad():
    output = model(input_batch)
print(output.shape) # 1개의 이미지에 대해 1000개의 클래스 점수

# softmax로 1000개의 클래스에 대한 확률을 얻을 수 있음
percentage = torch.nn.functional.softmax(output, dim=1)[0] * 100

그러면 pretrained 모델은 순식간에 답을 내린다.

바로 "금붕어" 라고 외치는 것이 아니라 "1번 돌맹이일 확률은 0.000001, 2번 금붕어일 확률은 0.99998, 3번 강아지일 확률은 0.00001...1000번 컴퓨터일 확률은 0.00000001" 과 같이 답을 내린다. 사실 엄밀히 말하자면 확률값이 아니라 score인데 바로 다음줄에서 softmax를 통해 확률값을 구하니 확률이라해도 무방할 것 같다.

 

pretrained가 답을 얘기할 때 1000개의 보기에 대한 대답을 보두 확인할 수는 없으니 top5만 뽑아보도록 하자.

# 가장 높은 확률을 가지는 카테고리 top5
for i in output[0].topk(5)[1]:
    print('=====')
    print(f'인덱스{i.item()}')
    print(f'클래스{imagnet_labels[i]}')
    print(f'확률{percentage[i].item():.6f}%')

나의 경우에는 goldfish가 앞도적으로 0.9999987, 퍼센트로 표현하면 99.99987% 값이 나왔다.

이렇게 resnet pretrained 모델을 사용하면 처음 보는 이미지라도 쉽게 분류할 수 있다.

 

 

참고

 

 

profile

매직코드

@개발법사

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!