[논문] ImageNet Classification with Deep Convolutional Neural Networks - 2

ByungJik_Oh·2026년 2월 9일

[Paper Review]

목록 보기
2/3
post-thumbnail

Abstract

AlexNet은 LSVRC-2010 대회에서 Top-1 오류율과 Top-5 오류율에서 37.5%와 17.0%를, ILSVRC-2012 대회에서 Top-5 오류율 15.3%를 달성하였다.

이 신경망은 6천만개의 파라미터와 65만개의 뉴런을 가졌으며, 5개의 Convolution layer, Max-pooling layer 일부, 3개의 FC-layer, 마지막엔 1000-way softmax로 구성되어 있다.

학습을 빠르게하기 위해 non-saturating neuron(ReLU)과 GPU 기반 Convolution 연산을 사용하였다.

FC-layer에서의 과적합을 방지하기 위해 Dropout 기법을 사용하였다.


1 Introduction

  • 본 논문의 기여

    • 가장 큰 CNN 중 하나를 ImageNet subset으로 학습시켜 ILSVRC-2010, ISVRC-2012 대회에서 가장 좋은 성능을 달성.
    • 2D Convolution을 포함한 CNN 학습 전반에 고도로 최적화된 GPU 구현 및 공개적으로 제공.
    • 학습시간 단축, 성능 향상을 위한 새롭고 일반적이지 않은 특징 포함. (Section 3)
    • 120만장의 데이터에서 과적합을 방지하기 위한 효과적인 기술 사용. (Section 4)
  • 최종 신경망 구조

    • 5개의 Convolution layer, 3개의 FC-layer
    • 이들의 깊이는 매우 중요함. (모델의 전체 파라미터의 1%도 포함하지 않지만 하나라도 제거하면 성능 저하)
  • 모델 크기의 한계

    • GPU 메모리 양
    • 감당가능한 학습시간

2 The Dataset

  • 데이터 셋 특징

    • ImageNet은 약 22000개의 카테고리에 속한 150만장 이상의 라벨링된 고해상도 이미지들로 이루어짐.
    • 이미지들은 웹에서 수집되었고, 인간들에 의해 라벨링됨.
  • ILSVRC

    • ILSVRC 대회에서 각 1000개 카테고리에 속한 약 1000장의 이미지로 이루어진 ImageNet의 subset을 사용함.
    • 전체적으로 약 120만장의 학습 이미지, 5만장의 검증 이미지, 15만장의 테스트 이미지로 이루어짐.
    • ImageNet에서는 일반적으로 Top-1 오류율과 Top-5 오류율 제시.
  • 입력 전처리

    • 우리의 신경망은 일정한 입력차원을 필요로 하지만 ImageNet은 다양한 해상도의 이미지들로 구성됨.
    • 따라서 256x256 크기로 down-sampling.
    • 직사각형 이미지 → 짧은 변의 길이가 256이 되도록 rescale 후 이미지 중앙의 256x256 추출.
    • 전체 학습 이미지의 평균 픽셀값을 substract, 추가적인 전처리 X
    • 기본 RGB 값 그대로 학습에 사용.

3 The Architecture

신경망은 5개의 Convolution layer, 3개의 FC-layer로 구성되어 있으며, Section 3.1 ~ 3.4는 중요도 순으로 정렬되어 있다.

3.1 ReLU Nonlinearity

  • 활성화 함수

    • 기존 신경망의 활성화 함수는 f(x)=tanh(x)f(x) = tanh(x) 또는 f(x)=(1+e1)1f(x) = (1+e^{-1})^{-1}(sigmoid) 이다.
    • 학습 시간 측면에서 경사 하강법을 사용할 경우, 위와 같은 saturating nonlinearity는 f(x)=max(0,x)f(x) = max(0,x)(ReLU)와 같은 non-saturating nonlinearity보다 느리다.
    • ReLU를 사용하는 깊은 CNN은 tanh 함수를 사용할 때보다 몇 배 더 빠르게 학습한다.

위 그림은 4-layer CNN이 CIFAR-10 데이터셋에서 학습 오류 25%에 도달할 때까지 필요한 반복의 수를 보여준다.

이때 ReLU(실선)을 사용한 CNN이 tanh(점선)을 사용한 CNN보다 6배 빠르게 25% 학습 오류에 도달하는 것을 볼 수 있다.

이는 기존의 saturating neuron을 사용했다면 대규모 CNN 학습은 불가능했을 것이다.

  • 기존 연구와 차별점

    • Jarrett et al.
    • f(x)=tanh(x)f(x) = |tanh(x)| + contrast normalization + local average pooling 방법이 Caltech-101 데이터셋에서 좋은 성능을 보임.
    • 그러나 여기서 핵심은 과적합 방지.
    • 본 논문에서의 ReLU의 효과는 학습 시간 단축.

3.2 Training on Multiple GPUs

  • GPU 병렬화

    • GTX 580 GPU는 3GB의 메모리를 가지고 있으며, 이는 120만개의 학습 예제를 하나의 GPU로 학습시키기에 부족하다.
    • 따라서 두 개의 GPU 사용.
    • 최신 GPU는 직접적으로 다른 메모리에 읽고 쓰기 가능.
  • 병렬화 구조

    • 커널을 절반씩 GPU에 배치.
    • GPU는 특정 레이어에서만 통신한다.
    • ex) 레이어 3의 커널들은 레이어 2의 모든 커널 맵으로부터 입력을 받음.
    • ex) 레이어 4의 커널들은 동일한 GPU에 있는 레이어 3의 커널 맵에서만 입력을 받음.

이는 GPU간 통신 비용과 연산량 사이의 밸런스 (통신을 많이하면 느려지고, 너무 적으면 성능이 떨어짐.)를 맞추기 위함이고, 이는 cross-validation으로 최적 연결 구조를 찾을 수 있었다.

  • 기존 연구와 차별점

    • Ciresan et al.
    • column들이 모두 독립적
    • 그러나 AlexNet의 경우 부분적으로 연결되어 있음.

하나의 GPU를 사용했을 때보다 Top-1, Top-5 오류율이 1.7%, 1.2% 감소되었으며, 학습 시간도 단축되었다.

3.3 Local Response Normalization

ReLU 함수는 입력 정규화를 필요로 하지 않으며, 양의 입력 들어왔을때 해당 뉴런에서 학습이 이루어진다. 그러나 다음과 같은 로컬 정규화 방법은 일반화에 도움이 된다는 것을 알 수 있다.

bx,yi=ax,yi/(k+αj=max(0,in/2)min(N1,i+n/2)(ax,yj)2)βb_{x,y}^i=a_{x,y}^i/\bigg(k+\alpha\sum_{j=max(0,i-n/2)}^{min(N-1,i+n/2)}(a_{x,y}^j)^2\bigg)^{\beta}
k=2,n=5,α=104,β=0.75k=2, n=5, \alpha=10^{-4}, \beta=0.75
  • 로컬 정규화 방법

    • 어떤 뉴런이 크게 활성화되면 주변 뉴런들이 억제되도록 하는 방식.
    • 따라서 인접한 뉴런들을 경쟁시키는 방식이다.
    • bx,yib_{x,y}^i: 정규화 된 최종 출력
    • ax,yia_{x,y}^i: ReLU를 통과한 원래 값
    • jj: ii 주변의 인접한 뉴런들
    • NN: 전체 뉴런 수
    • kk, nn, α\alpha, β\beta: 하이퍼파라미터
  • 기존 연구와 차별점

    • Jarret et al.
    • contrast normalization과 달리 평균을 빼지 않음.
    • brightness normalization이라고 표현.

이 정규화 방법을 사용했을 때 ImageNet에서 Top-1, Top-5 오류율 1.4%, 1.2% 감소, CIFAR-10에서 정규화하지 않았을 때 테스트 오류율 13%에서 11%로 감소

3.4 Overlapping Pooling

풀링(Pooling)은 근처 뉴런들을 요약하는 연산이다.
ss: stride
zz: 풀링 윈도우 크기

  • non-Overlapping Pooling

    • s=zs=z로, 윈도우가 겹치지 않음.
  • Overlapping Pooling

    • s<zs < z로, 윈도우가 겹침.
    • AlexNet에선 s=2s=2, z=3z=3

Overlapping Pooling이 적용되었을 때 Top-1, Top-5 오류율 0.4%, 0.3% 감소, 그리고 과적합에 더 강한 모습을 보임.

3.5 Overall Architecture

Conv Layer 5개, FC-Layer 3개, 1000-way softmax (출력), Multinomial Logistic Regression

  • Conv1 Layer

    • 입력: 224x224x3 (실제 입력은 227x227x3이다.)
    • 출력: 55x55x96 ((22711)/4+1=55)(227-11)/4 + 1=55))
    • 11x11x3 필터 96개, stride 4
  • Conv2 Layer

    • 입력: LRN, MaxPooling 거친 (55x55x96 → 27x27x96) Conv1 Layer의 출력
    • 출력: 27x27x256
    • 5x5x48 필터 256개, stride 1
    • 같은 GPU에 있는 Layer와 연결
  • Conv3 Layer

    • 입력: LRN, MaxPooling 거친 (27x27x256 → 13x13x256) Conv2 Layer의 출력
    • 출력: 13x13x384
    • 3x3x192 필터 384개, stride 1
    • 모든 Layer와 연결
  • Conv4 Layer

    • 입력: Conv3 Layer의 출력
    • 출력: 13x13x384
    • 3x3x192 필터 384개, stride 1
    • 같은 GPU에 있는 Layer와 연결
  • Conv5 Layer

    • 입력: Conv4 Layer의 출력
    • 출력: 13x13x256
    • 3x3x192 필터 256개, stride 1
    • 같은 GPU에 있는 Layer와 연결
  • FC1 Layer

    • 입력: MaxPooling 거친 (13x13x256 → 6x6x256) Conv5 Layer의 출력을 Flatten한 9216차원
    • 출력: 4096개
    • 모든 Layer와 연결
  • FC2 Layer

    • 입력: FC1 Layer의 입력
    • 출력: 4096개
    • 모든 Layer와 연결
  • FC3 Layer

    • 입력 FC2 Layer의 출력
    • 출력: 1000개 → softmax를 통해 확률분포 출력
    • 모든 Layer와 연결

4 Reducing Overfitting

AlexNet은 6천만개의 파라미터를 가지며, 아래는 과적합을 방지하기 위한 방법을 소개한다.

4.1 Data Augmentation

데이터 증강에 있어서 필요한 연산은 GPU가 학습하는 동안 CPU에서 변형된 이미지가 생성되기 때문에 연산 비용 측면에서 자유롭다.

  • translation & hrizontal reflection

    • 256x256 크기의 원본 이미지에서 무작위의 224x224 크기의 패치를 잘라냄. (좌우 반전 추가)
    • 이는 신경망의 첫번째 입력이 224x224x3인 이유이다.
    • 결과적으로 데이터셋의 크기를 2048배 키운 효과.
    • 테스트 시에는 각 모서리와 중간 5개에 각각 반전시킨 이미지 총 10개에 대해 softmax 결과 평균.
    • 저자들은 이 방법이 없었다면 신경망의 크기를 줄여야 했을것이라고 하였다.
  • RGB color PCA

    • 객체의 정체성은 빛 밝기나 색상의 변화에 따라 다양해지지 않는다.
    • 전체 데이터셋의 RGB 픽셀값에 PCA 수행
    • 이후 각 픽셀 Ixy=[IxyR,IxyG,IxyB]TI_{xy}=[I_{xy}^R,I_{xy}^G,I_{xy}^B]^T[p1,p2,p3][α1λ1,α2λ2,α3λ3]T[p_1,p_2,p_3][α_1λ_1,α_2λ_2,α_3λ_3]^T를 더함
    • pip_i: RGB 픽셀값의 3x3 공분산 행렬의 ii번째 고유 벡터
    • λiλ_i: RGB 픽셀값의 3x3 공분산 행렬의 ii번째 고유값
    • αiα_i: 평균 0, 표준편차 0.1인 가우시안 분포에서 추출한 난수 (이미지마다 새로 추출)
    • 원래 이미지에 자연스러운 조명 변화 추가
    • Top-1 오류율 1% 이상 감소

4.2 Dropout

성능을 올리는 가장 좋은 방법은 앙상블이다. 그러나 AlexNet과 같이 큰 신경망에서는 시간과비용적인 측면에서 현실적으로 불가능하다. 그래서 사용한 방법이 Dropout.

  • Dropout

    • 각 뉴런의 출력을 0.5의 확률로 0으로 두는 것.
    • Dropout된 뉴런은 순전파, 역전파 모두 참여 X
    • 따라서 뉴런 간의 co-adaption 방지 → 더욱 견고한 특징 학습
    • → 매 반복마다 신경망은 다른 구조로 학습되지만, 가중치는 공유한다.
    • 비교적 적은 비용으로 앙상블 효과.
    • 테스트 시에는 모든 뉴런을 사용하지만, 출력에 0.5를 곱한 값을 사용. (매우 많은 Dropout network 예측의 기하평균)

Dropout 없이는 매우 심한 과적합이 발생했으며, 여기선 처음 두 FC-Layer에서 적용하였다.


5 Details of learning

  • optimizer: stochastic gradient descent
  • batch size: 128
  • momentum: 0.9
  • weight decay: 0.0005
  • weight init: 평균 0, 표준편차 0.01 가우시안 분포
  • bias init: Conv 2, 4, 5, FC-Layer → 1, 나머지 0
  • LR init: 0.01
  • epochs: 약 90
vi+1      :=      0.9vi0.0005ϵwiϵ<Lwwi>Div_{i+1} \;\;\; := \;\;\; 0.9 \cdot v_i - 0.0005 \cdot \epsilon w_i - \epsilon \cdot \bigg< \frac{\partial L}{\partial w} |_{w_i} \bigg>_{D_i}
wi+1      :=      wi+vi+1w_{i+1} \;\;\; := \;\;\; w_i + v_{i+1}
  • weight decay

    • 가중치 감소는 단지 규제가 아닌 학습 오류도 줄여준다.
  • Bias 초기화

    • Conv 2, 4, 5와 FC-Layer의 Bias 1로 초기화
    • 이는 ReLU 함수에 양의 입력을 제공함으로써 학습의 초기 단계 속도 가속.
  • Learing Rate

    • validation 성능이 개선되지 않으면 learing rate 10으로 나눔.
    • 0.01로 초기화되었으며, 종료전에 3번 감소됨.

구현

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F

from torchsummary import summary

from torch.utils import data
import torchvision.datasets as datasets
import torchvision.transforms as transforms

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# ReLU 활성화 함수 (Section 3.1) - 학습 시간 단축
# LRN (Section 3.3) - 과적합 방지
# Overlapping Pooling (Section 3.4) - 과적합 방지
# Dropout (Section 4.2) - 과적합 방지
# Bias Initialization (Section 5) - ReLU 함수에 양의 입력 -> 초기 단계 학습 가속
class AlexNet(nn.Module):
    def __init__(self, num_classes=1000): # Section 3.5
        # 입력 크기: (b x 3 x 227 x 227)
        # 논문 상에선 입력 크기가 224라고 작성되어 있지만 실제론 227
        super().__init__()

        self.conv = nn.Sequential(
            nn.Conv2d(in_channels=3, out_channels=96, kernel_size=11, stride=4), # (b x 96 x 55 x 55)
            nn.ReLU(), # Section 3.1
            nn.LocalResponseNorm(size=5, alpha=0.0001, beta=0.75, k=2), # Section 3.3
            nn.MaxPool2d(kernel_size=3, stride=2), # Section 3.4 (b x 96 x 27 x 27)
            nn.Conv2d(in_channels=96, out_channels=256, kernel_size=5, stride=1, padding=2), # (b x 256 x 27 x 27)
            nn.ReLU(),
            nn.LocalResponseNorm(size=5, alpha=0.0001, beta=0.75, k=2),
            nn.MaxPool2d(kernel_size=3, stride=2), # (b x 256 x 13 x 13)
            nn.Conv2d(in_channels=256, out_channels=384, kernel_size=3, stride=1, padding=1), # (b x 384 x 13 x 13)
            nn.ReLU(),
            nn.Conv2d(in_channels=384, out_channels=384, kernel_size=3, stride=1, padding=1), # (b x 384 x 13 x 13)
            nn.ReLU(),
            nn.Conv2d(in_channels=384, out_channels=256, kernel_size=3, stride=1, padding=1), # (b x 256 x 13 x 13)
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=3, stride=2) # (b x 256 x 6 x 6)
        )

        self.fc = nn.Sequential(
            nn.Dropout(p=0.5), # Section 4.2
            nn.Linear(in_features=(256 * 6 * 6), out_features=4096),
            nn.ReLU(),
            nn.Dropout(p=0.5),
            nn.Linear(in_features=4096, out_features=4096),
            nn.ReLU(),
            nn.Linear(in_features=4096, out_features=num_classes)
        )

        self.init_wb()

    def init_wb(self): # Section 5
        for layer in self.conv:
            if isinstance(layer, nn.Conv2d):
                nn.init.normal_(layer.weight, mean=0, std=0.01)
                nn.init.constant_(layer.bias, 0)

        nn.init.constant_(self.conv[4].bias, 1)
        nn.init.constant_(self.conv[10].bias, 1)
        nn.init.constant_(self.conv[12].bias, 1)

        for layer in self.fc:
            if isinstance(layer, nn.Linear):
                nn.init.normal_(layer.weight, mean=0, std=0.01)
                nn.init.constant_(layer.bias, 1)

    def forward(self, x):
        x = self.conv(x)
        x = torch.flatten(x, 1)
        x = self.fc(x)
        return x
# Details of Learning (Section 5)
# weight decay: 학습 오류 감소
model = AlexNet().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(
    params=model.parameters(),
    lr=0.01,
    momentum=0.9,
    weight_decay=0.0005
)

lr_scheduler = optim.lr_scheduler.StepLR(
    optimizer=optimizer,
    step_size=30,
    gamma=0.1
)

summary(model, input_size=(3, 227, 227), batch_size=128)
----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
            Conv2d-1          [128, 96, 55, 55]          34,944
              ReLU-2          [128, 96, 55, 55]               0
 LocalResponseNorm-3          [128, 96, 55, 55]               0
         MaxPool2d-4          [128, 96, 27, 27]               0
            Conv2d-5         [128, 256, 27, 27]         614,656
              ReLU-6         [128, 256, 27, 27]               0
 LocalResponseNorm-7         [128, 256, 27, 27]               0
         MaxPool2d-8         [128, 256, 13, 13]               0
            Conv2d-9         [128, 384, 13, 13]         885,120
             ReLU-10         [128, 384, 13, 13]               0
           Conv2d-11         [128, 384, 13, 13]       1,327,488
             ReLU-12         [128, 384, 13, 13]               0
           Conv2d-13         [128, 256, 13, 13]         884,992
             ReLU-14         [128, 256, 13, 13]               0
        MaxPool2d-15           [128, 256, 6, 6]               0
          Dropout-16                [128, 9216]               0
           Linear-17                [128, 4096]      37,752,832
             ReLU-18                [128, 4096]               0
          Dropout-19                [128, 4096]               0
           Linear-20                [128, 4096]      16,781,312
             ReLU-21                [128, 4096]               0
           Linear-22                [128, 1000]       4,097,000
================================================================
Total params: 62,378,344
Trainable params: 62,378,344
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 75.48
Forward/backward pass size (MB): 1885.10
Params size (MB): 237.95
Estimated Total Size (MB): 2198.54
----------------------------------------------------------------
# Data Augmentation (Section 4.1) - 과적합 방지
# mean substraction only, PCA color augmentation, 10 crop test 구현 X
TRAIN_DIR = ''
VAL_DIR = ''
TEST_DIR = ''

train_transform = transforms.Compose([
    transforms.Resize(256),
    transforms.RandomCrop(227),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize(
        mean=[0.485, 0.456, 0.406],
        std=[0.229, 0.224, 0.225]
    )
])

train_dataset = datasets.ImageFolder(TRAIN_DIR, transform=train_transform)

train_loader = data.DataLoader(
    train_dataset,
    batch_size=128,
    shuffle=True,
    num_workers=8,
    pin_memory=True
)

val_transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(227),
    transforms.ToTensor(),
    transforms.Normalize(
        mean=[0.485, 0.456, 0.406],
        std=[0.229, 0.224, 0.225]
    )
])

val_dataset = datasets.ImageFolder(VAL_DIR, transform=val_transform)

val_loader = data.DataLoader(
    val_dataset,
    batch_size=128,
    shuffle=False,
    num_workers=8,
    pin_memory=True
)

test_transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(227),
    transforms.ToTensor(),
    transforms.Normalize(
        mean=[0.485, 0.456, 0.406],
        std=[0.229, 0.224, 0.225]
    )
])

test_dataset = datasets.ImageFolder(TEST_DIR, transform=test_transform)

test_loader = data.DataLoader(
    test_dataset,
    batch_size=128,
    shuffle=False,
    num_workers=8,
    pin_memory=True
)
# Epoch (Section 5) - Roughly 90
best_acc = 0
for epoch in range(90):
    model.train()

    train_loss = 0
    correct = 0
    total = 0

    for img, cls in train_loader:
        img, cls = img.to(device), cls.to(device)

        output = model(img)
        loss = criterion(output, cls)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        train_loss += loss.item() * img.size(0)
        _, preds = torch.max(output, 1)
        correct += (preds == cls).sum().item()
        total += cls.size(0)
    
    train_loss /= total
    train_acc = correct / total

    model.eval()

    val_loss = 0
    correct = 0
    total = 0

    with torch.no_grad():
        for img, cls in val_loader:
            img, cls = img.to(device), cls.to(device)

            output = model(img)
            loss = criterion(output, cls)

            val_loss += loss.item() * img.size(0)
            _, preds = torch.max(output, 1)
            correct += (preds == cls).sum().item()
            total += cls.size(0)

    val_loss /= total
    val_acc = correct / total

    if (epoch + 1) % 10 == 0:
        print(f'Epoch: {epoch + 1}/90 - Train Loss: {train_loss:.4f} - Train Acc: {train_acc:.4f} - Val Loss {val_loss:.4f} - Val Acc {val_acc:.4f}')

    if val_acc > best_acc:
        best_acc = val_acc
        torch.save(model.state_dict(), "best_alexnet.pth")
        print("Best Model Updated")

    torch.save({
        'epoch': epoch,
        'model_state_dict': model.state_dict(),
        'optimizer_state_dict': optimizer.state_dict(),
        'best_acc': best_acc
    }, "alexnet_checkpoint.pth")

    lr_scheduler.step()
model.load_state_dict(torch.load("best_alexnet.pth"))
model.to(device)

model.eval()

test_loss = 0
correct = 0
total = 0

with torch.no_grad():
    for img, cls in test_loader:
        img, cls = img.to(device), cls.to(device)

        output = model(img)
        loss = criterion(output, cls)

        test_loss += loss.item() * img.size(0)
        _, preds = torch.max(output, 1)
        correct += (preds == cls).sum().item()
        total += cls.size(0)

test_loss /= total
test_acc = correct / total

print(f'Test Loss {test_loss:.4f} - Test Acc {test_acc:.4f}')

Reference

https://proceedings.neurips.cc/paper_files/paper/2012/file/c399862d3b9d6b76c8436e924a68c45b-Paper.pdf

profile
精進 "정성을 기울여 노력하고 매진한다"

0개의 댓글