Step 6 CNN과 이미지넷 혁명 - 고양이 vs 강아지 분류

합성곱 신경망으로 실제 이미지를 분류해보자

1. ImageNet: 딥러닝의 진짜 시작

MNIST로 손글씨를 인식했다면, 이제는 실제 세상의 사진을 분류해봅시다. 2012년, 한 대회가 AI의 역사를 바꿨습니다. 바로 ImageNet Large Scale Visual Recognition Challenge (ILSVRC)입니다.

🏆 ImageNet ILSVRC 2012: 역사를 바꾼 대회

대회 개요

📉 2012년 이전의 상황

2010년: Top-5 Error 28.2% (전통적 컴퓨터 비전 기법)
2011년: Top-5 Error 25.8% (손으로 만든 특징 추출 + SVM)
• 매년 조금씩 개선되었지만, 벽에 부딪힌 상태였습니다.

"이미지는 너무 복잡해서 기계가 사람처럼 인식하는 건 불가능하다"는 회의론이 지배적이었죠.

🚀 2012년 AlexNet: 게임 체인저의 등장

30% 25% 20% 15% 10% 2010 2011 2012 2015 ImageNet Top-5 Error Rate 28.2% 25.8% 16.4% AlexNet 3.57% ↓ 9.4%p 감소! (역대 최대 성능 향상) 인간 수준 (약 5%)

🎊 충격적인 결과

AlexNet (Alex Krizhevsky, Ilya Sutskever, Geoffrey Hinton):
Top-5 Error: 16.4% (2위: 26.2%)
• 2위와의 격차: 거의 10%p - 전례 없는 압승!
• 이전 우승자들을 압도하는 성능

이 결과는 "딥러닝의 시대"를 여는 역사적 순간이었습니다.
2013년부터 모든 우승자가 딥러닝 기반 CNN을 사용하게 되었죠.

💡 AlexNet이 성공한 이유

2. CNN이란? MLP와 무엇이 다른가?

MNIST에서는 MLP를 사용했습니다. 28×28 이미지를 784개의 1차원 벡터로 펼쳤죠. 하지만 이 방식은 이미지의 공간 정보를 무시합니다.

❌ MLP의 한계

원본 이미지 🐱 224×224×3 Flatten 1D 벡터 150,528개 원소 224×224×3 ⚠️ 문제점 1. 공간 정보 손실 • 이웃한 픽셀의 관계 무시 2. 엄청난 파라미터 수 • 150k → 512 연결만 해도 77M개의 가중치 필요!

이미지를 1차원으로 펼치면:
• 고양이 귀가 서로 붙어있다는 정보가 사라집니다
• 224×224×3 = 150,528개의 연결이 필요합니다
• 이미지 크기가 커질수록 파라미터가 폭발적으로 증가합니다

✅ CNN의 핵심 아이디어

Convolutional Neural Network (합성곱 신경망)는 이미지의 지역적 패턴을 학습합니다.

입력 이미지 필터 3×3 또는 5×5 작은 윈도우 → 슬라이딩 특징 맵 가장자리, 질감 등 패턴 검출 ✨ 장점 1. 공간 정보 보존 • 이웃 픽셀 관계 유지 2. 파라미터 공유 • 같은 필터를 전체에 적용 3. 위치 불변성 • 고양이가 어디 있든 인식

3. CNN의 핵심 구성 요소

CNN은 크게 세 가지 층으로 구성됩니다. 각각의 역할을 자세히 알아봅시다.

🔍 1. Convolutional Layer (합성곱층)

이미지에서 특정 패턴을 찾아내는 역할을 합니다.

🎯 동작 원리:

1. 필터(커널): 작은 행렬 (예: 3×3, 5×5)
2. 슬라이딩: 이미지 위를 좌→우, 위→아래로 이동
3. 내적 연산: 필터와 이미지 일부의 원소별 곱셈 후 합산
4. 특징 맵 생성: 각 위치에서 계산된 값들의 모음

합성곱 연산 예시 입력 (5×5) 필터 (3×3) 1 0 -1 1 0 -1 1 0 -1 세로 가장자리 검출 * (합성곱) 특징 맵 (3×3) v₁ v₂ v₃ v₄ v₅ v₆ v₇ v₈ v₉ 핵심 개념 • Stride: 필터 이동 간격 (보통 1 또는 2) • Padding: 가장자리 처리 (크기 유지 목적) 출력 크기 = (입력 - 필터 + 2×Padding) / Stride + 1 예: (5 - 3 + 0) / 1 + 1 = 3
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           # 패딩 (크기 유지)
)

📉 2. Pooling Layer (풀링층)

특징 맵의 크기를 줄여서 계산량을 감소시키고, 중요한 특징만 남깁니다.

Max Pooling (2×2, stride=2) 입력 (4×4) 1 3 2 4 5 8 6 9 2 4 1 3 7 9 5 8 Max 출력 (2×2) 8 9 9 8 Max Pooling 효과 1. 크기 감소: 4×4 → 2×2 (75% 감소) 2. 가장 강한 특징 보존 3. 위치 변화에 강인함 → 고양이가 약간 움직여도 인식 4. 과적합 방지 💡 Average Pooling도 있지만, Max Pooling이 더 일반적
# PyTorch에서 풀링층 정의
pool = nn.MaxPool2d(
    kernel_size=2,  # 2x2 윈도우
    stride=2        # 2칸씩 이동 (겹치지 않음)
)

🔗 3. Fully Connected Layer (완전연결층)

추출된 특징들을 기반으로 최종 분류를 수행합니다. (MLP와 동일)

합성곱층과 풀링층이 "특징 추출기"라면,
완전연결층은 "분류기" 역할을 합니다.

• 2D 특징 맵을 1D로 펼침 (Flatten)
• 일반 신경망처럼 모든 뉴런이 연결됨
• 최종 출력: 클래스별 확률 (Softmax)

4. 전체 CNN 아키텍처

이제 합성곱층, 풀링층, 완전연결층을 결합해서 전체 CNN을 구성해봅시다.

🏗️ 일반적인 CNN 구조

CNN 아키텍처 흐름 Input 224×224 ×3 RGB 이미지 Conv 64 filters 3×3 + ReLU Pool 2×2 Max Conv 128 filters 3×3 + ReLU Pool 2×2 Max ... Flatten 2D → 1D FC 512 + ReLU Output 1000 classes 특징 추출 (Feature Extraction) 분류 (Classification) 점점 깊어질수록: 크기↓, 채널(필터)↑ 초반: 가장자리/질감 → 후반: 복잡한 패턴/객체

🎨 AlexNet 아키텍처

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

5. PyTorch로 CNN 구현하기

이제 PyTorch로 실제 CNN을 구현해봅시다. AlexNet보다 간단한 버전으로 시작하겠습니다.

🔨 기본 CNN 모델 정의

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)

📚 주요 PyTorch CNN 함수들

함수/클래스 역할 주요 파라미터
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

🎯 CNN 학습 시 중요 포인트

6. CNN 실습: 고양이 vs 강아지 분류

MNIST보다 훨씬 어려운 실제 사진으로 CNN의 위력을 체험해봅시다. 바로 유명한 Dogs vs. Cats 데이터셋입니다!

🐱🐶 Dogs vs. Cats 데이터셋

2013년 Kaggle 대회로 유명해진 데이터셋으로, 컴퓨터 비전의 "Hello World" 같은 존재입니다.

💡 이 데이터셋을 사용하는 이유

현실적: MNIST처럼 단순하지 않은 실제 사진
접근성: Kaggle에서 무료로 다운로드 가능
적절한 난이도: CNN 없이는 어렵지만, CNN으로 충분히 도전 가능
교육적: ImageNet에 포함된 클래스들 (실제 딥러닝 연구와 연결)

기본 CNN으로는 70-75% 정도 성능을 기대할 수 있습니다.
→ 이 정도면 나쁘지 않지만... 더 좋게 만들 수는 없을까요? (Step 7에서 계속!)

📦 데이터셋 다운로드 및 준비

실습을 위해 데이터를 준비하는 방법은 크게 2가지입니다.

✅ 방법 1: Kaggle에서 직접 다운로드 (권장)

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, ...

⚡ 방법 2: Kaggle API 사용 (빠름)

# 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/

🔬 방법 3: 소규모 샘플 데이터 (빠른 실험용)

전체 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')))} 장")

📸 PyTorch로 데이터 로드하기

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% 이상의 정확도를 달성해봅시다! 🚀