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 구현하기 참고)
'AI·머신러닝 > 딥러닝·비전' 카테고리의 다른 글
| OpenCV 기초 - 이미지 읽기, 속성, 도형·마우스 이벤트 실습 (0) | 2025.12.31 |
|---|---|
| 전이 학습 실전 - Alien vs Predator · 콘크리트 균열 탐지, AlexNet · VGG19 활용 (0) | 2025.12.29 |
| CNN과 손글씨 도형 분류 - 합성곱 신경망, 학습, FastAPI·Gradio 서빙 (0) | 2025.12.23 |
| Multi-class Weather 이미지 분류 실습 - ImageFolder, DataLoader, 완전연결 신경망 (0) | 2025.12.22 |
| 논리 회귀부터 손글씨 숫자·퍼셉트론까지 - 분류, DataLoader, 데이터 증강, MLP 실습 (0) | 2025.12.20 |