본문 바로가기

책/파이토치 트랜스포머를 활용한 자연어 처리와 컴퓨터비전 심층학습

08 이미지 분류 (1) AlexNet

이미지 분류는 이미지에서 객체(Object)나 장면(Scenes)과 같은 요소를 인식하고 분류하는 알고리즘이다. 지도 학습의 한 유형으로, 이미지와 이미지에 해당하는 클래스를 할당해 데이터세트를 구성한다.

 

이미지 분류는 크게 단일, 다중 클래스 분류, 다중 레이블 분류로 나뉜다.

 

단일 클래스 분류 : 이미지 안에 서로 다른 여러 객체가 존재하더라도 하나의 대표 클래스로만 이미지를 분류한다. 예를 들어 개의 이미지가 주어진다면 개인지 아닌지 즉, 참인지 거짓인지를 판별한다.

 

다중 클래스 분류 : 개인지, 고양이인지를 분류하거나 개의 품종을 분류하는 일을 수행한다.

 

다중 레이블 분류 : 입력 이미젱서 여러 클래스를 예측한다. 예를 들어 이미지 안에서 개, 소파, 커튼 등을 검출할 수 있다.

 

 

AlexNet은 ILSVRC(ImageNet Large Scale Visual Recognition Challenge) 2012에서 우승한 합성곱 신경망 구조의 모델이다. 이전의 얕은 구조의 모델들의 인식 오류율이 약 26%였지만, 알렉스넷은 16%까지 낮췄다.

 

AlexNet 구조

 

알렉스넷은 이미지의 특징 추출을 위해 합성곱과 최댓값 풀링 계층을 활용하며, 이후 완전 연결 계층을 통해 클래스를 분류한다. 

 

알렉스넷의 구조를 보면 순전파 과정에서 특징 맵의 차원 수는 증가하고, 크기는 감소한다. 특징 맵의 차원 수가 늘어날수록 모델의 표현력이 증가하며, 특징 맵의 크기를 줄여 연산량을 줄일 수 있다.

 

 

1. LeNet-5와 AlexNet

알렉스넷의 구조는 1998년에 개발된 LeNet-5의 구조와 유사하다. 먼저 LeNet-5의 구조를 알아보자.

종류 출력 크기 커널 크기 스트라이드 활성화 함수
입력 입력 32x32 - - -
C1 합성곱 6x28x28 5x5 1 sigmoid
S2 맥스 풀링 6x14x14 2x2 2 -
C3 합성곱 16x10x10 5x5 1 sigmoid
S4 맥스 풀링 16x5x5 2x2 2 -
FC5 완전 연결 120 - - sigmoid
FC6 완전 연결 84 - - sigmoid
출력 출력 10 - - softmax

 

 

알렉스넷의 구조는 다음과 같다.

종류 출력 크기 커널 크기 스트라이드 활성화 함수
입력 입력 3x227x227 - - -
C1 합성곱 96x55x55 11x11 4 ReLU
S2 맥스 풀링 96x27x27 3x3 2 -
C3 합성곱 256x27x27 5x5 1 ReLU
S4 맥스 풀링 256x13x13 3x3 2 -
C5 합성곱 384x13x13 3x3 1 ReLU
C6 합성곱 384x13x13 3x3 1 ReLU
C7 합성곱 256x13x13 3x3 1 ReLU
S8 맥스 풀링 256x6x6 3x3 2 -
FC9 완전 연결 4096 - - ReLU
FC10 완전 연결 4096 - - ReLU
출력 출력 1000 - - softmax

 

주요한 차이는 활성화 함수인데 LeNet-5는 리스케일링된 로지스틱 시그모이드 함수를 사용했고, 알렉스넷은 ReLU를 사용했다. 시그모이드는 기울기 소실 문제를 유발하지만 ReLU는 그렇지 않아 더 깊은 계층을 쌓을 수 있게 됐다.

 

알렉스넷은 C1, C3에서 활성화 함수 다음에 LRN(Local Response Normalization)이 사용됐다. LRN은 활성화 맵의 각 위치에서 주변 픽셀 값들에 대한 정규화를 수행한다. 현대의 심층 신경망에서는 주로 배치 정규화가 더 많이 사용되는 경향이 있다.

 

또한 완전 연결 층에서 드롭아웃을 활용하여 과대적합을 완화했다.

 

 

2. 모델 실습

torchvision과 torchinfo 라이브러리를 통해 사전 학습된 모델을 불러오고, 모델 요약을 확인할 수 있다.

 

 

알렉스넷 모델 구조 출력

from torchvision import models
from torchinfo import summary

model = models.alexnet(weights="AlexNet_Weights.IMAGENET1K_V1")
summary(model, input_size=(1, 3, 224, 224), device="cpu")
==========================================================================================
Layer (type:depth-idx)                   Output Shape              Param #
==========================================================================================
AlexNet                                  [1, 1000]                 --
├─Sequential: 1-1                        [1, 256, 6, 6]            --
│    └─Conv2d: 2-1                       [1, 64, 55, 55]           23,296
│    └─ReLU: 2-2                         [1, 64, 55, 55]           --
│    └─MaxPool2d: 2-3                    [1, 64, 27, 27]           --
│    └─Conv2d: 2-4                       [1, 192, 27, 27]          307,392
│    └─ReLU: 2-5                         [1, 192, 27, 27]          --
│    └─MaxPool2d: 2-6                    [1, 192, 13, 13]          --
│    └─Conv2d: 2-7                       [1, 384, 13, 13]          663,936
│    └─ReLU: 2-8                         [1, 384, 13, 13]          --
│    └─Conv2d: 2-9                       [1, 256, 13, 13]          884,992
│    └─ReLU: 2-10                        [1, 256, 13, 13]          --
│    └─Conv2d: 2-11                      [1, 256, 13, 13]          590,080
│    └─ReLU: 2-12                        [1, 256, 13, 13]          --
│    └─MaxPool2d: 2-13                   [1, 256, 6, 6]            --
├─AdaptiveAvgPool2d: 1-2                 [1, 256, 6, 6]            --
├─Sequential: 1-3                        [1, 1000]                 --
│    └─Dropout: 2-14                     [1, 9216]                 --
│    └─Linear: 2-15                      [1, 4096]                 37,752,832
│    └─ReLU: 2-16                        [1, 4096]                 --
│    └─Dropout: 2-17                     [1, 4096]                 --
│    └─Linear: 2-18                      [1, 4096]                 16,781,312
│    └─ReLU: 2-19                        [1, 4096]                 --
│    └─Linear: 2-20                      [1, 1000]                 4,097,000
==========================================================================================
Total params: 61,100,840
Trainable params: 61,100,840
Non-trainable params: 0
Total mult-adds (M): 714.68
==========================================================================================
Input size (MB): 0.60
Forward/backward pass size (MB): 3.95
Params size (MB): 244.40
Estimated Total Size (MB): 248.96
==========================================================================================

 

출력 결과는 알렉스넷에 (1, 3, 224, 224) 크기의 텐서를 입력했을 때 계층마다 출력하는 특징 맵의 크기와 필요한 매개변수의 수를 보여준다. 합성곱에 비해 완전 연결 계층은 매개변수의 수가 훨씬 많은데, 매개 변수의 수가 많아지면 과대적합이 발생할 확률이 높아 드롭아웃을 사용한다.

 

 

다음 표는 AlexNet_Weights.IMAGENET1K_V1에 대한 모델 정보를 요약했다.

속성
acc@1 56.522
acc@5 79.066
입력 이미지 최소 크기 63 x 63
매개변수의 수 61,100,840
카테고리(클래스) 수 1,000
GFLOPS 0.71
파일 크기 233.1MB

 

acc@n이란 상위 n개 레이블에 대한 예측 정확도를 의미한다.

 

예를 들어 개 이미지를 입력했을 때 예측된 결과가 [고양이(50%), 말(30%), 소(10%), 개(5%), 늑대(1%), ...]라고 가정한다면, acc@1은 상위 1개 레이블에 대한 정확도를 특정하므로 예측이 틀렸다고 볼 수 있다.

 

acc@5는 상위 5개에 대한 예측 정확도를 계산하므로 예측된 확률이 5%로 낮더라도 예측이 성공했다고 볼 수 있다.

 

GFLOPS초당 기가 부동 소수점 연산(Giga-Floating Point Operations Per Second)의 약자로, 해당 모델에 대한 컴퓨팅 성능을 측정한 값을 의미한다. 값이 높을수록 복잡한 계산 및 데이터 처리 작업을 더 빠르게 수행할 수 있다.

 

torchvision.models에서 제공하는 이미지 분류 모델과 정보는 pytorch docs에서 확인할 수 있다.

 

 

클래스 정보 파일 불러오기

파이토치 허브에서 이미지넷 클래스 정보 파일을 다운로드할 수 있다.

 

import urllib.request

url = "https://raw.githubusercontent.com/pytorch/hub/master/imagenet_classes.txt"
filename = "imagenet_classes.txt"

# 파일 다운로드
urllib.request.urlretrieve(url, filename)

with open("/content/imagenet_classes.txt", "r") as file:
    classes = file.read().splitlines()

print(f"클래스 개수 : {len(classes)}")
print(f"첫 번째 클래스 레이블 : {classes[0]}")
클래스 개수 : 1000
첫 번째 클래스 레이블 : tench

 

urllib.request.urlretrieve 함수로 다운받을 url과 로컬에서 저장할 이름을 매개변수로 넘겨주면 파일을 다운로드 받을 수 있다.

 

 

이미지 데이터 전처리

import torch
from PIL import Image
from torchvision import models, transforms

transform = transforms.Compose(
    [
        transforms.Resize((224, 224)),
        transforms.ToTensor(),
        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
    ]
)

device = "cuda" if torch.cuda.is_available() else "cpu"
model = models.alexnet(weights="AlexNet_Weights.IMAGENET1K_V1").eval().to(device)

tensors = []
files = list(IMAGE_PATH.glob("*.jpg"))

for file in files:
  image = Image.open(file)
  tensors.append(transform(image))

tensors = torch.stack(tensors)
print(f"input tensor size : {tensors.shape}")

 

torch.stack은 리스트나 텐서를 입력받아 새로운 차원을 추가한다. 여기서는 리스트에 (3, 224, 224) 사이즈의 텐서가 5개 append되어 있으므로 이를 (5, 3, 224, 224) 사이즈의 텐서로 변환시켜준다.

 

 

알렉스넷 모델 추론

outputs의 shape은 (5, 1000)이기 때문에 class를 의미하는 -1차원으로 소프트맥스를 적용해 확률을 얻는다.

topk(n) 메서드로 상위 n개에 대한 확률과 인덱스를 얻는다.

import numpy as np
import matplotlib.pyplot as plt
from torch.nn import functional as F

with torch.no_grad():
  outputs = model(tensors.to(device))
  probs = F.softmax(outputs, dim=-1)
  top_probs, top_idxs = probs.topk(5)

top_probs = top_probs.detach().cpu().numpy()
top_idxs = top_idxs.detach().cpu().numpy()
top_classes = np.array(classes)[top_idxs]

for idx, (cls, prob) in enumerate(zip(top_classes, top_probs)):
  path = files[idx]
  image = Image.open(path)
  title = str(path).split("/")[-1].split(".")[0]
  plt.imshow(image)
  plt.title(title)
  plt.show()
  for c, p in zip(cls, prob):
    print(f" - {c:<30} : {p * 100:>5.2f}%")