MNIST로 손글씨를 인식했다면, 이제는 실제 세상의 사진을 분류해봅시다. 2012년, 한 대회가 AI의 역사를 바꿨습니다. 바로 ImageNet Large Scale Visual Recognition Challenge (ILSVRC)입니다.
대회 개요
📉 2012년 이전의 상황
• 2010년: Top-5 Error 28.2% (전통적 컴퓨터 비전 기법)
• 2011년: Top-5 Error 25.8% (손으로 만든 특징 추출 + SVM)
• 매년 조금씩 개선되었지만, 벽에 부딪힌 상태였습니다.
"이미지는 너무 복잡해서 기계가 사람처럼 인식하는 건 불가능하다"는 회의론이 지배적이었죠.
🎊 충격적인 결과
AlexNet (Alex Krizhevsky, Ilya Sutskever, Geoffrey Hinton):
• Top-5 Error: 16.4% (2위: 26.2%)
• 2위와의 격차: 거의 10%p - 전례 없는 압승!
• 이전 우승자들을 압도하는 성능
이 결과는 "딥러닝의 시대"를 여는 역사적 순간이었습니다.
2013년부터 모든 우승자가 딥러닝 기반 CNN을 사용하게 되었죠.
MNIST에서는 MLP를 사용했습니다. 28×28 이미지를 784개의 1차원 벡터로 펼쳤죠. 하지만 이 방식은 이미지의 공간 정보를 무시합니다.
이미지를 1차원으로 펼치면:
• 고양이 귀가 서로 붙어있다는 정보가 사라집니다
• 224×224×3 = 150,528개의 연결이 필요합니다
• 이미지 크기가 커질수록 파라미터가 폭발적으로 증가합니다
Convolutional Neural Network (합성곱 신경망)는 이미지의 지역적 패턴을 학습합니다.
CNN은 크게 세 가지 층으로 구성됩니다. 각각의 역할을 자세히 알아봅시다.
이미지에서 특정 패턴을 찾아내는 역할을 합니다.
🎯 동작 원리:
1. 필터(커널): 작은 행렬 (예: 3×3, 5×5)
2. 슬라이딩: 이미지 위를 좌→우, 위→아래로 이동
3. 내적 연산: 필터와 이미지 일부의 원소별 곱셈 후 합산
4. 특징 맵 생성: 각 위치에서 계산된 값들의 모음
import torch.nn as nn # PyTorch에서 합성곱층 정의 conv_layer = nn.Conv2d( in_channels=3, # 입력 채널 (RGB) out_channels=64, # 출력 채널 (필터 개수) kernel_size=3, # 필터 크기 (3x3) stride=1, # 이동 간격 padding=1 # 패딩 (크기 유지) )
특징 맵의 크기를 줄여서 계산량을 감소시키고, 중요한 특징만 남깁니다.
# PyTorch에서 풀링층 정의 pool = nn.MaxPool2d( kernel_size=2, # 2x2 윈도우 stride=2 # 2칸씩 이동 (겹치지 않음) )
추출된 특징들을 기반으로 최종 분류를 수행합니다. (MLP와 동일)
합성곱층과 풀링층이 "특징 추출기"라면,
완전연결층은 "분류기" 역할을 합니다.
• 2D 특징 맵을 1D로 펼침 (Flatten)
• 일반 신경망처럼 모든 뉴런이 연결됨
• 최종 출력: 클래스별 확률 (Softmax)
이제 합성곱층, 풀링층, 완전연결층을 결합해서 전체 CNN을 구성해봅시다.
2012년 ImageNet 우승 모델인 AlexNet의 구조를 살펴봅시다.
| Layer | Type | Output Size | Parameters |
|---|---|---|---|
| Input | - | 227×227×3 | - |
| Conv1 | 11×11, 96 filters, stride 4 | 55×55×96 | 35K |
| Pool1 | 3×3 max pool, stride 2 | 27×27×96 | 0 |
| Conv2 | 5×5, 256 filters | 27×27×256 | 614K |
| Pool2 | 3×3 max pool, stride 2 | 13×13×256 | 0 |
| Conv3 | 3×3, 384 filters | 13×13×384 | 885K |
| Conv4 | 3×3, 384 filters | 13×13×384 | 1.3M |
| Conv5 | 3×3, 256 filters | 13×13×256 | 884K |
| Pool3 | 3×3 max pool, stride 2 | 6×6×256 | 0 |
| FC1 | Fully Connected | 4096 | 37.7M |
| FC2 | Fully Connected | 4096 | 16.8M |
| FC3 | Fully Connected (Output) | 1000 | 4.1M |
📊 총 파라미터: 약 62M (6200만 개!)
주목할 점:
• 대부분의 파라미터가 FC층에 집중 (약 94%)
• Conv층은 상대적으로 파라미터가 적지만, 계산량은 많음
• 점진적으로 크기는 줄고, 채널은 증가 (3 → 96 → 256 → 384 → 256)
• 5개의 Conv층 + 3개의 FC층 = 8층 Deep Network
이제 PyTorch로 실제 CNN을 구현해봅시다. AlexNet보다 간단한 버전으로 시작하겠습니다.
import torch import torch.nn as nn import torch.nn.functional as F class SimpleCNN(nn.Module): def __init__(self): super(SimpleCNN, self).__init__() # 합성곱층 정의 self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1) # 3채널 → 32채널 self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1) # 32채널 → 64채널 self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1) # 풀링층 정의 self.pool = nn.MaxPool2d(2, 2) # 2×2, stride 2 # 완전연결층 정의 # 224×224 이미지 → pool 3번 → 28×28×128 self.fc1 = nn.Linear(128 * 28 * 28, 512) self.fc2 = nn.Linear(512, 10) # 10개 클래스 분류 # Dropout (과적합 방지) self.dropout = nn.Dropout(0.5) def forward(self, x): # Conv1 → ReLU → Pool x = self.pool(F.relu(self.conv1(x))) # 224×224×3 → 112×112×32 # Conv2 → ReLU → Pool x = self.pool(F.relu(self.conv2(x))) # 112×112×32 → 56×56×64 # Conv3 → ReLU → Pool x = self.pool(F.relu(self.conv3(x))) # 56×56×64 → 28×28×128 # Flatten: (batch, 128, 28, 28) → (batch, 128*28*28) x = x.view(-1, 128 * 28 * 28) # FC1 → ReLU → Dropout x = self.dropout(F.relu(self.fc1(x))) # FC2 (Output) x = self.fc2(x) return x # 모델 생성 model = SimpleCNN() print(model)
| 함수/클래스 | 역할 | 주요 파라미터 |
|---|---|---|
nn.Conv2d() |
2D 합성곱층 | in_channels, out_channels, kernel_size, stride, padding |
nn.MaxPool2d() |
Max Pooling | kernel_size, stride |
nn.AvgPool2d() |
Average Pooling | kernel_size, stride |
nn.BatchNorm2d() |
배치 정규화 (학습 안정화) | num_features |
nn.Dropout() |
과적합 방지 | p (드롭 확률, 보통 0.5) |
F.relu() |
ReLU 활성화 함수 | - |
x.view() |
텐서 형태 변환 (Flatten 등) | 새로운 shape |
MNIST보다 훨씬 어려운 실제 사진으로 CNN의 위력을 체험해봅시다. 바로 유명한 Dogs vs. Cats 데이터셋입니다!
2013년 Kaggle 대회로 유명해진 데이터셋으로, 컴퓨터 비전의 "Hello World" 같은 존재입니다.
💡 이 데이터셋을 사용하는 이유
• 현실적: MNIST처럼 단순하지 않은 실제 사진
• 접근성: Kaggle에서 무료로 다운로드 가능
• 적절한 난이도: CNN 없이는 어렵지만, CNN으로 충분히 도전 가능
• 교육적: ImageNet에 포함된 클래스들 (실제 딥러닝 연구와 연결)
→ 기본 CNN으로는 70-75% 정도 성능을 기대할 수 있습니다.
→ 이 정도면 나쁘지 않지만... 더 좋게 만들 수는 없을까요? (Step 7에서 계속!)
실습을 위해 데이터를 준비하는 방법은 크게 2가지입니다.
1단계: Kaggle 계정 생성 (무료)
2단계: Dogs vs. Cats 대회 페이지 방문
3단계: "Download All" 클릭 → train.zip (약 540MB) 다운로드
4단계: 압축 해제 후 train 폴더에 25,000장의 이미지 확인
• 파일명: cat.0.jpg, cat.1.jpg, ..., dog.0.jpg, dog.1.jpg, ...
# 1. Kaggle API 설치 pip install kaggle # 2. Kaggle API 토큰 설정 # - Kaggle 사이트 → Account → Create New API Token # - kaggle.json 파일을 ~/.kaggle/ 에 저장 # 3. 데이터셋 다운로드 kaggle competitions download -c dogs-vs-cats # 4. 압축 해제 unzip train.zip -d data/dogs-vs-cats/
전체 25,000장이 부담스럽다면, 샘플 데이터로 먼저 실험해볼 수 있습니다.
고양이 1,000장 + 강아지 1,000장 = 총 2,000장 정도면 충분히 학습 가능합니다.
train/ 폴더에서 앞부분 2,000개 파일만 복사해서 사용하면 됩니다.
다운로드한 데이터를 학습에 사용하기 좋게 폴더 구조로 정리합니다.
# 원본 구조 (Kaggle 다운로드 후) train/ ├── cat.0.jpg ├── cat.1.jpg ├── ... ├── cat.12499.jpg ├── dog.0.jpg ├── dog.1.jpg ├── ... └── dog.12499.jpg # 우리가 만들 구조 (PyTorch ImageFolder 형식) data/ ├── train/ │ ├── cats/ │ │ ├── cat.0.jpg │ │ ├── cat.1.jpg │ │ └── ... (10,000장) │ └── dogs/ │ ├── dog.0.jpg │ ├── dog.1.jpg │ └── ... (10,000장) └── val/ ├── cats/ │ └── ... (2,500장) └── dogs/ └── ... (2,500장)
📝 Python 코드로 자동 분할:
import os import shutil from pathlib import Path # 원본 경로 original_dir = Path('train') # 새 구조 만들기 base_dir = Path('data/dogs-vs-cats') train_dir = base_dir / 'train' val_dir = base_dir / 'val' for split in [train_dir, val_dir]: for category in ['cats', 'dogs']: (split / category).mkdir(parents=True, exist_ok=True) # 고양이 이미지 분할 (10,000 train + 2,500 val) cat_files = sorted(list(original_dir.glob('cat.*.jpg'))) for i, file in enumerate(cat_files[:12500]): dest = train_dir / 'cats' if i < 10000 else val_dir / 'cats' shutil.copy(file, dest / file.name) # 강아지 이미지 분할 (10,000 train + 2,500 val) dog_files = sorted(list(original_dir.glob('dog.*.jpg'))) for i, file in enumerate(dog_files[:12500]): dest = train_dir / 'dogs' if i < 10000 else val_dir / 'dogs' shutil.copy(file, dest / file.name) print("데이터 분할 완료!") print(f"Train: {len(list(train_dir.glob('*/*.jpg')))} 장") print(f"Val: {len(list(val_dir.glob('*/*.jpg')))} 장")
from torchvision import datasets, transforms from torch.utils.data import DataLoader # 데이터 전처리 (Data Augmentation 포함) train_transform = transforms.Compose([ transforms.Resize((224, 224)), # 모든 이미지를 224x224로 리사이즈 transforms.RandomHorizontalFlip(), # 50% 확률로 좌우 반전 transforms.RandomRotation(10), # ±10도 회전 transforms.ToTensor(), # PIL → Tensor 변환 transforms.Normalize( mean=[0.485, 0.456, 0.406], # ImageNet 평균 std=[0.229, 0.224, 0.225] # ImageNet 표준편차 ) ]) val_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] ) ]) # 데이터셋 로드 train_dataset = datasets.ImageFolder( 'data/dogs-vs-cats/train', transform=train_transform ) val_dataset = datasets.ImageFolder( 'data/dogs-vs-cats/val', transform=val_transform ) # 데이터 로더 train_loader = DataLoader( train_dataset, batch_size=32, shuffle=True, num_workers=4 # 병렬 로딩 ) val_loader = DataLoader( val_dataset, batch_size=32, shuffle=False, num_workers=4 ) print(f"Train: {len(train_dataset)} images") print(f"Val: {len(val_dataset)} images") print(f"Classes: {train_dataset.classes}") # ['cats', 'dogs']
💡 핵심 포인트:
• ImageFolder: 폴더 구조를 보고 자동으로 레이블 지정
• Data Augmentation: 훈련 데이터만 적용 (검증은 원본 그대로)
• Normalization: ImageNet 통계값 사용 (전이학습 준비)
• 배치 크기 32: GPU 메모리에 맞게 조절 가능
위 실습에서 우리가 만든 간단한 CNN으로 약 70-75% 정확도를 달성했습니다.
나쁘지 않지만, 더 좋게 만들 수는 없을까요?
문제점:
• 훈련 데이터가 20,000장이지만, 처음부터 학습하기엔 부족
• 네트워크를 더 깊게 만들면? → 과적합 발생
• 더 오래 학습하면? → 일정 수준 이상 안 올라감
💡 해결책: 전이학습 (Transfer Learning)
ImageNet으로 이미 학습된 모델(ResNet, VGG 등)을 가져와서
우리 데이터에 맞게 파인튜닝하면 어떨까요?
다음 Step 7에서 전이학습으로 95% 이상의 정확도를 달성해봅시다! 🚀