수민 '-'

플오그래밍

제가 작성하는 모든 글은 절대 상업적인 이용이 아니며, 그저 개인적인 공부 용도로만 사용하는 것임을 밝힙니다.

AlexNet과 CIFAR-10 이미지 분류 - ILSVRC, 전처리, 혼동 행렬 실습

2012년 ImageNet 우승 모델을 CIFAR-10으로 돌려보기

AlexNet은 2012년 ILSVRC(ImageNet Large Scale Visual Recognition Challenge)에서 우승한 CNN으로, 딥러닝이 컴퓨터 비전의 표준으로 자리 잡는 데 기여한 모델이다. ReLU, Dropout, 데이터 증강을 적극 활용했고, 5개의 합성곱 레이어와 3개의 완전 연결 레이어로 구성된다. 이번 포스트에서는 CIFAR-10 데이터셋으로 AlexNet을 직접 구현하고, 학습·평가·혼동 행렬(Confusion Matrix) 시각화까지 한 번에 다룬다.

(Alexnet 구현하기를 기반으로 재구성)


1. AlexNet이란?

AlexNet은 8개 레이어(합성곱 5개 + 완전 연결 3개)로 이루어진 CNN이다. ImageNet 1,000클래스 분류에서 Top-1 Error 37.5%, Top-5 Error 17.5%를 기록했고, 당시 전통적인 방법보다 큰 격차로 성능을 끌어올렸다. ReLU 활성화, Dropout, 데이터 증강으로 과적합을 줄였고, GPU 병렬 연산을 활용해 대규모 학습이 가능해졌다.

※ ImageNet LSVRC

ImageNet LSVRC(Large Scale Visual Recognition Challenge)는 이미지 인식·분류 기술을 겨루는 대회다. 2010년부터 매년 개최되었고, ImageNet 데이터셋(약 1,400만 장, 1,000클래스)을 기반으로 한다. 컴퓨터 비전과 딥러닝 기술 발전을 목표로 하며, 2012년 AlexNet이 딥러닝 기반으로 우수한 성능을 보이며 딥러닝 시대를 열었다.

※ Error Rate

  • Top-1 Error Rate: 예측 확률 1위 클래스가 정답이 아닐 확률. 예: 정답은 "고양이"인데 모델 1위가 "강아지"이면 Top-1 에러.
  • Top-5 Error Rate: 예측 상위 5개 안에 정답이 없을 확률. 상위 5개 중 하나라도 정답이면 성공으로 친다. 유사 클래스(치타 vs 표범 등)를 고려한 실용적 지표로 쓰인다.

2. CIFAR 데이터셋

CIFAR는 torchvision으로 제공되는 이미지 분류용 데이터셋이다.

  • CIFAR-10: 10클래스, 클래스당 6,000장, 총 60,000장, 32×32 컬러 이미지.
  • CIFAR-100: 100클래스, 클래스당 600장, 총 60,000장, 32×32 컬러.

PyTorch에서는 torchvision.datasets.CIFAR10으로 다운로드·학습/테스트 분리·transform을 적용할 수 있다. AlexNet은 원래 224×224 ImageNet용이므로, CIFAR-10의 32×32에 맞추려면 합성곱·풀링 후 Flatten 크기를 32×32 기준으로 조정해야 한다.


3. CIFAR-10 로드 및 통계

import numpy as np
import matplotlib.pyplot as plt
import torch
from torch.utils.data import DataLoader
from torch import nn
from torchvision import datasets
from torchvision.transforms import transforms
from torchvision.transforms.functional import to_pil_image

train_img = datasets.CIFAR10(
    root='data',
    train=True,
    download=True,
    transform=transforms.ToTensor(),
)

test_img = datasets.CIFAR10(
    root='data',
    train=False,
    download=True,
    transform=transforms.ToTensor(),
)

mean = train_img.data.mean(axis=(0, 1, 2)) / 255
std = train_img.data.std(axis=(0, 1, 2)) / 255
print(f'평균: {mean}, 표준편차: {std}')

4. 전처리 (정규화·증강)

학습용에는 RandomCrop, RandomHorizontalFlip을 넣고, ToTensor 후 Normalize(mean, std) 를 적용한다. 테스트용에는 증강 없이 ToTensor + Normalize만 적용한다.

transform_train = transforms.Compose([
    transforms.RandomCrop(size=train_img.data.shape[1], padding=4),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize(mean, std),
])

transform_test = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize(mean, std),
])

train_img = datasets.CIFAR10(
    root='data',
    train=True,
    download=True,
    transform=transform_train,
)

test_img = datasets.CIFAR10(
    root='data',
    train=False,
    download=True,
    transform=transform_test,
)

5. DataLoader 및 시각화

EPOCH = 10
BATCH_SIZE = 128
LEARNING_RATE = 1e-3
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("Using Device:", DEVICE)

train_loader = DataLoader(train_img, batch_size=BATCH_SIZE, shuffle=True)
test_loader = DataLoader(test_img, batch_size=BATCH_SIZE, shuffle=False)

train_features, train_labels = next(iter(train_loader))
print(f"Feature batch shape: {train_features.size()}")
print(f"Labels batch shape: {train_labels.size()}")

labels_map = {
    0: "plane", 1: "car", 2: "bird", 3: "cat", 4: "deer",
    5: "dog", 6: "frog", 7: "horse", 8: "ship", 9: "truck",
}

정규화된 이미지를 시각화할 때는 역정규화(denormalize) 가 필요하다.

def denormalize(img, mean, std):
    mean = torch.tensor(mean).view(3, 1, 1)
    std = torch.tensor(std).view(3, 1, 1)
    return img * std + mean

figure = plt.figure(figsize=(8, 8))
cols, rows = 5, 5
for i in range(1, cols * rows + 1):
    sample_idx = torch.randint(len(train_img), size=(1,)).item()
    img, label = train_img[sample_idx]
    img = denormalize(img, mean, std)
    figure.add_subplot(rows, cols, i)
    plt.title(labels_map[label])
    plt.axis('off')
    plt.imshow(to_pil_image(img))
plt.show()

6. AlexNet 모델 (CIFAR-10용)

원래 AlexNet은 224×224 입력 기준이다. CIFAR-10은 32×32이므로, 합성곱·풀링 후 공간 크기가 4×4가 되도록 구성하면 FC 입력은 256×4×4 = 4096이 된다.

  • Conv1: 3→96, 3×3, padding=1 → 32×32 → ReLU → MaxPool(2,2) → 16×16
  • Conv2: 96→256, 3×3, padding=1 → 16×16 → ReLU → MaxPool(2,2) → 8×8
  • Conv3: 256→384, 3×3, padding=1 → 8×8 → ReLU
  • Conv4: 384→384, 3×3, padding=1 → 8×8 → ReLU
  • Conv5: 384→256, 3×3, padding=1 → 8×8 → ReLU → MaxPool(2,2) → 4×4
  • FC1: 256×4×4 = 4096 → 4096, Dropout(0.5), ReLU
  • FC2: 4096 → 4096, Dropout(0.5), ReLU
  • FC3: 4096 → num_classes (10)
class AlexNet(nn.Module):
    def __init__(self, num_classes=10):
        super(AlexNet, self).__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 96, kernel_size=3, stride=1, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),

            nn.Conv2d(96, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),

            nn.Conv2d(256, 384, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),

            nn.Conv2d(384, 384, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),

            nn.Conv2d(384, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
        )
        self.classifier = nn.Sequential(
            nn.Linear(256 * 4 * 4, 4096),
            nn.Dropout(0.5),
            nn.ReLU(inplace=True),
            nn.Linear(4096, 4096),
            nn.Dropout(0.5),
            nn.ReLU(inplace=True),
            nn.Linear(4096, num_classes),
        )

    def forward(self, x):
        x = self.features(x)
        x = x.view(x.size(0), -1)
        x = self.classifier(x)
        return x

model = AlexNet().to(DEVICE)
print(model)

7. 학습·테스트 루프

loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE)

def train(train_loader, model, loss_fn, optimizer):
    model.train()
    size = len(train_loader.dataset)
    for batch, (X, y) in enumerate(train_loader):
        X, y = X.to(DEVICE), y.to(DEVICE)
        pred = model(X)
        loss = loss_fn(pred, y)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        if batch % 100 == 0:
            loss_val, current = loss.item(), batch * len(X)
            print(f'loss: {loss_val:>7f}  [{current:>5d}]/{size:5d}')

def test(test_loader, model, loss_fn):
    model.eval()
    size = len(test_loader.dataset)
    num_batches = len(test_loader)
    test_loss, correct = 0, 0
    with torch.no_grad():
        for X, y in test_loader:
            X, y = X.to(DEVICE), y.to(DEVICE)
            pred = model(X)
            test_loss += loss_fn(pred, y).item()
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()
    test_loss /= num_batches
    correct /= size
    print(f"Test Error: Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:8f}\n")

for i in range(EPOCH):
    print(f"Epoch {i+1}\n------------------------")
    train(train_loader, model, loss_fn, optimizer)
    test(test_loader, model, loss_fn)
print("Done!")

8. 혼동 행렬 (Confusion Matrix)

혼동 행렬은 행을 정답(True Label), 열을 예측(Predicted Label) 로 두어, 클래스별로 맞춘 개수·잘못 맞춘 개수를 한눈에 보는 표다. sklearn.metrics.confusion_matrix로 계산하고, 정규화해 비율로 그리면 어떤 클래스를 자주 헷갈리는지 파악하기 좋다.

from sklearn.metrics import confusion_matrix
import itertools

model.eval()
ylabel = np.array([])
ypred_label = np.array([])

with torch.no_grad():
    for inputs, targets in test_loader:
        inputs, targets = inputs.to(DEVICE), targets.to(DEVICE)
        outputs = model(inputs)
        _, predicted = outputs.max(1)
        ylabel = np.concatenate((ylabel, targets.cpu().numpy()))
        ypred_label = np.concatenate((ypred_label, predicted.cpu().numpy()))

cnf_matrix = confusion_matrix(ylabel, ypred_label)
print(cnf_matrix)

혼동 행렬 시각화

  • 정규화: 각 행(정답 클래스)별로 합이 1이 되도록 하면, “정답이 i일 때 예측이 j인 비율”을 볼 수 있다.
  • 대각선 합 / 전체 합 = 전체 정확도, 1 - 정확도 = misclass 비율.
def plot_confusion_matrix(cm, target_names=None, cmap=None, normalize=True,
                          labels=True, title='Confusion matrix'):
    accuracy = np.trace(cm) / float(np.sum(cm))
    misclass = 1 - accuracy

    if cmap is None:
        cmap = plt.get_cmap('Blues')

    if normalize:
        cm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]

    plt.figure(figsize=(8, 6))
    plt.imshow(cm, interpolation='nearest', cmap=cmap)
    plt.title(title)
    plt.colorbar()

    thresh = cm.max() / 1.5 if normalize else cm.max() / 2
    if target_names is not None:
        tick_marks = np.arange(len(target_names))
        plt.xticks(tick_marks, target_names)
        plt.yticks(tick_marks, target_names)

    if labels:
        for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
            if normalize:
                plt.text(j, i, "{:0.4f}".format(cm[i, j]),
                        horizontalalignment="center",
                        color="white" if cm[i, j] > thresh else "black")
            else:
                plt.text(j, i, "{:,}".format(cm[i, j]),
                        horizontalalignment="center",
                        color="white" if cm[i, j] > thresh else "black")

    plt.tight_layout()
    plt.ylabel('True label')
    plt.xlabel(f'Predicted label  accuracy={accuracy:0.4f}  misclass={misclass:0.4f}')
    plt.show()

plot_confusion_matrix(cnf_matrix,
                     target_names=list(labels_map.values()),
                     title='Confusion matrix, trained by AlexNet')

마치며

  • AlexNet은 ILSVRC 2012 우승 모델로, ReLU·Dropout·데이터 증강으로 과적합을 줄이고 GPU로 대규모 학습을 했다. Top-1/Top-5 Error Rate로 평가한다.
  • CIFAR-10은 32×32 컬러 10클래스 데이터셋으로, AlexNet 구조를 32×32에 맞추려면 Conv·Pool 후 공간 크기를 4×4로 두고 FC 입력을 256×4×4로 맞추면 된다.
  • 전처리: 데이터셋 평균·표준편차로 Normalize, 학습 시 RandomCrop·RandomHorizontalFlip으로 증강.
  • 혼동 행렬로 클래스별 정답/오답 비율을 보고, 어떤 클래스를 자주 헷갈리는지 해석할 수 있다.

다음으로는 VGG, ResNet, EfficientNet 등 더 깊은 CNN이나 전이 학습을 적용해 보면 좋다.

(Alexnet 구현하기 참고)