CNN (Convolutional Neural Network, 합성곱 신경망)

핵심 인사이트 (3줄 요약)

CNN은 합성곱 연산으로 이미지의 공간적 특징(엣지→패턴→객체)을 계층적으로 추출하는 딥러닝 아키텍처. LeNet→AlexNet→VGG→ResNet→EfficientNet→ConvNeXt 순으로 발전. 객체 탐지(YOLO)·의료 영상(U-Net)·자율주행 등 실세계 비전 응용의 핵심이다.


Ⅰ. 개요 (필수: 200자 이상)

개념: CNN(Convolutional Neural Network)은 이미지나 시계열 데이터에서 공간적 패턴을 학습하는 딥러닝 네트워크로, 합성곱(Convolution)·풀링(Pooling)·완전연결(FC) 계층을 조합하여 계층적 특징을 추출한다.

💡 비유: CNN은 "스캐너로 그림 분석하기" 같아요. 처음엔 선을 찾고, 다음엔 모서리, 그다음엔 눈코입, 최종엔 얼굴을 인식하죠! 각 단계에서 더 복잡한 패턴을 학습해요.

등장 배경 (필수: 3가지 이상 기술):

  1. 기존 문제점 - MLP 파라미터 폭발: 224×224×3 이미지 → 150K 입력, 완전연결 시 파라미터 수천만 개
  2. 기술적 필요성 - 공간 정보 보존: 이미지를 1D로 펼치면 위치 정보 손실 → 합성곱으로 공간 구조 유지
  3. 시장/산업 요구 - ImageNet 챌린지: 2012년 AlexNet이 기존 방식 대비 10%+ 향상, 딥러닝 르네상스 시작

핵심 목적: 이미지 공간 계층 특징 학습, 파라미터 효율화 (가중치 공유), 이동 불변성 확보


Ⅱ. 구성 요소 및 핵심 원리 (필수: 가장 상세하게)

CNN 계층 구조 (필수: ASCII 아트):

┌─────────────────────────────────────────────────────────────────────────┐
│                    CNN 계층별 특징 추출                                   │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   입력 이미지                Conv Layer 1             Conv Layer 2     │
│   ┌─────────────┐          ┌─────────────┐          ┌─────────────┐   │
│   │ ░░▓▓░░░░░░ │          │ ╱╲  ╱╲  ╱╲ │          │  ┌───┐     │   │
│   │ ░░▓▓░░░░░░ │   →      │╱  ╲╱  ╲╱  ╲│   →      │  │ ○ │     │   │
│   │ ░░░░▓▓▓▓░░ │          │▓▓▓▓▓▓▓▓▓▓▓│          │  └───┘     │   │
│   │ ░░░░▓▓▓▓░░ │          │▓▓▓▓▓▓▓▓▓▓▓│          │    ◆       │   │
│   └─────────────┘          └─────────────┘          └─────────────┘   │
│   224×224×3               112×112×64               56×56×128          │
│                                                                         │
│   특징: 픽셀값             특징: 엣지, 선             특징: 패턴, 부품  │
│                                                                         │
│   ┌─────────────────────────────────────────────────────────────────┐  │
│   │                      CNN 레이어 흐름                             │  │
│   │                                                                 │  │
│   │  Input → Conv → ReLU → Pool → Conv → ReLU → Pool → ... → FC    │  │
│   │    ↓       ↓        ↓      ↓      ↓        ↓      ↓         ↓   │  │
│   │  224×    222×     222×  111×   109×    109×   54×       1000   │  │
│   │  224×3   222×96   ×96   ×96   109×256  ×256   ×256      (클래스)│  │
│   │                                                                 │  │
│   └─────────────────────────────────────────────────────────────────┘  │
│                                                                         │
│   깊이별 학습 특징:                                                      │
│   Layer 1-2: 엣지, 색상, 그라디언트                                     │
│   Layer 3-4: 텍스처, 곡선, 간단한 패턴                                  │
│   Layer 5-6: 부품 (눈, 코, 입, 바퀴, 창문)                              │
│   Layer 7+:  객체 (얼굴, 자동차, 개, 고양이)                            │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

[합성곱 연산 상세]

입력: 5×5          필터: 3×3         출력: 3×3
┌───────────────┐   ┌─────────┐   ┌─────────────┐
│ 1  2  3  4  5 │   │ 1  0  1 │   │ 12 16 17   │
│ 5  6  7  8  9 │   │ 0  1  0 │   │ 24 28 29   │
│ 9 10 11 12 13 │ ⊛ │ 1  0  1 │   │ 36 40 41   │
│13 14 15 16 17 │   └─────────┘   └─────────────┘
│17 18 19 20 21 │
└───────────────┘

출력[0,0] = 1×1 + 2×0 + 3×1 + 5×0 + 6×1 + 7×0 + 9×1 + 10×0 + 11×1
         = 1 + 3 + 6 + 9 + 11 = 30  (bias 추가 후 활성화)

공식: output[i,j] = ΣΣ X[i+m, j+n] × W[m,n] + b
                    m n

CNN 핵심 구성요소 (필수: 표):

구성 요소역할/기능특징파라미터
Convolution특징 추출가중치 공유, 지역 연결K²×C_in×C_out
Activation (ReLU)비선형성max(0, x)0
Pooling (Max/Avg)공간 축소이동 불변성 강화0
BatchNorm정규화학습 안정화2×C
Dropout정규화과적합 방지0
Fully Connected분류최종 예측(N+1)×C
Softmax확률 변환클래스 확률0

주요 CNN 아키텍처 진화 (필수: ASCII 아트):

┌─────────────────────────────────────────────────────────────────────────┐
│                    CNN 아키텍처 진화 계보                                 │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   1989          2012         2014        2015        2019    2022      │
│    │             │            │           │           │       │         │
│    ▼             ▼            ▼           ▼           ▼       ▼         │
│  LeNet-5      AlexNet      VGG-16     ResNet-152  EfficientNet ConvNeXt│
│    │             │            │           │           │       │         │
│   5층          8층          19층        152층       81층     86층       │
│  60K파라      60M파라      138M파라    60M파라     66M파라   90M파라   │
│                                                                         │
│   우편번호     ImageNet     ImageNet    ImageNet    ImageNet  ImageNet │
│   인식        우승         2위         우승         SOTA     SOTA      │
│                                                                         │
│   ┌─────────────────────────────────────────────────────────────────┐  │
│   │ 핵심 혁신                                                        │  │
│   ├─────────────────────────────────────────────────────────────────┤  │
│   │ LeNet:    CNN 개념 증명, 가중치 공유                             │  │
│   │ AlexNet:  ReLU, Dropout, GPU 학습, Data Augmentation            │  │
│   │ VGG:      3×3 필터 통일, 깊이 증가                               │  │
│   │ ResNet:   Skip Connection (F(x) + x), 100+층 가능               │  │
│   │ DenseNet: Dense Connection, Feature Reuse                       │  │
│   │ EfficientNet: Compound Scaling (width/depth/resolution)         │  │
│   │ ConvNeXt: ViT 설계 차용 (LayerNorm, GELU, PatchEmbed)           │  │
│   └─────────────────────────────────────────────────────────────────┘  │
│                                                                         │
│   [ResNet Skip Connection]                                              │
│                                                                         │
│         x ─────────────────────────────┐                               │
│         │                              │                               │
│         ▼                              │                               │
│    ┌─────────┐                         │                               │
│    │ Conv    │                         │                               │
│    │ BN      │                         │                               │
│    │ ReLU    │                         │                               │
│    └────┬────┘                         │                               │
│         │                              │                               │
│         ▼                              │                               │
│    ┌─────────┐                         │                               │
│    │ Conv    │                         │                               │
│    │ BN      │                         │                               │
│    └────┬────┘                         │                               │
│         │                              │                               │
│         └────────────────► [+] ◄───────┘                               │
│                              │                                          │
│                              ▼                                          │
│                           ReLU                                          │
│                              │                                          │
│                              ▼                                          │
│                            F(x) + x                                     │
│                                                                         │
│   효과: 그래디언트 직접 전파, Degradation Problem 해결                   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

핵심 알고리즘/공식 (해당 시 필수):

[합성곱 출력 크기 계산]

O = (W - K + 2P) / S + 1

W: 입력 크기 (width/height)
K: 필터 크기 (kernel size)
P: 패딩 (padding)
S: 스트라이드 (stride)

예시: 224×224 입력, 3×3 필터, padding=1, stride=1
O = (224 - 3 + 2×1) / 1 + 1 = 224 (크기 유지)

예시: 224×224 입력, 3×3 필터, padding=1, stride=2
O = (224 - 3 + 2×1) / 2 + 1 = 112 (절반 축소)

[풀링 출력 크기]

O = (W - K) / S + 1

Max Pooling 2×2, stride=2: 224 → 112

[파라미터 수 계산]

Conv 파라미터 = K² × C_in × C_out + C_out (bias)

예: 3×3 Conv, 64→128 채널
파라미터 = 9 × 64 × 128 + 128 = 73,856

FC 파라미터 = N_in × N_out + N_out

예: 512 → 1000 클래스
파라미터 = 512 × 1000 + 1000 = 513,000

[수용 영역 (Receptive Field)]

RF_l = RF_{l-1} + (K_l - 1) × ∏_{i=1}^{l-1} S_i

Layer 1 (3×3, s=1): RF = 3
Layer 2 (3×3, s=1): RF = 5
Layer 3 (3×3, s=2): RF = 9
...

깊은 층일수록 더 넓은 영역 볼 수 있음

코드 예시 (필수: Python 순수 구현):

from dataclasses import dataclass, field
from typing import List, Tuple, Optional, Callable
from enum import Enum
import math
import random

# ============================================================
# CNN 구현 (순수 Python)
# ============================================================

def relu(x: float) -> float:
    """ReLU 활성화 함수"""
    return max(0, x)

def leaky_relu(x: float, alpha: float = 0.01) -> float:
    """Leaky ReLU"""
    return x if x > 0 else alpha * x

def softmax(x: List[float]) -> List[float]:
    """Softmax 함수"""
    max_x = max(x)
    exp_x = [math.exp(xi - max_x) for xi in x]
    sum_exp = sum(exp_x)
    return [e / sum_exp for e in exp_x]


@dataclass
class Tensor3D:
    """3D 텐서 (채널 × 높이 × 너비)"""
    channels: int
    height: int
    width: int
    data: List[List[List[float]]] = None

    def __post_init__(self):
        if self.data is None:
            self.data = [[[0.0] * self.width
                          for _ in range(self.height)]
                         for _ in range(self.channels)]

    @classmethod
    def zeros(cls, channels: int, height: int, width: int) -> 'Tensor3D':
        return cls(channels, height, width)

    @classmethod
    def random(cls, channels: int, height: int, width: int,
               scale: float = 0.1) -> 'Tensor3D':
        data = [[[random.gauss(0, scale) for _ in range(width)]
                 for _ in range(height)]
                for _ in range(channels)]
        return cls(channels, height, width, data)

    def get(self, c: int, h: int, w: int) -> float:
        return self.data[c][h][w]

    def set(self, c: int, h: int, w: int, val: float) -> None:
        self.data[c][h][w] = val


@dataclass
class Filter:
    """합성곱 필터"""
    in_channels: int
    out_channels: int
    kernel_size: int
    weights: List[List[List[List[float]]]] = None  # [out_c][in_c][h][w]
    bias: List[float] = None

    def __post_init__(self):
        if self.weights is None:
            scale = math.sqrt(2.0 / (self.in_channels * self.kernel_size ** 2))
            self.weights = [
                [[[random.gauss(0, scale) for _ in range(self.kernel_size)]
                  for _ in range(self.kernel_size)]
                 for _ in range(self.in_channels)]
                for _ in range(self.out_channels)
            ]
        if self.bias is None:
            self.bias = [0.0] * self.out_channels


class Conv2D:
    """2D 합성곱 레이어"""

    def __init__(self, in_channels: int, out_channels: int,
                 kernel_size: int, stride: int = 1, padding: int = 0):
        self.in_channels = in_channels
        self.out_channels = out_channels
        self.kernel_size = kernel_size
        self.stride = stride
        self.padding = padding
        self.filter = Filter(in_channels, out_channels, kernel_size)

    def forward(self, x: Tensor3D) -> Tensor3D:
        """순전파"""
        # 패딩 적용
        if self.padding > 0:
            x = self._pad(x, self.padding)

        # 출력 크기 계산
        out_h = (x.height - self.kernel_size) // self.stride + 1
        out_w = (x.width - self.kernel_size) // self.stride + 1

        output = Tensor3D.zeros(self.out_channels, out_h, out_w)

        # 합성곱 연산
        for oc in range(self.out_channels):
            for oh in range(out_h):
                for ow in range(out_w):
                    val = 0.0
                    for ic in range(self.in_channels):
                        for kh in range(self.kernel_size):
                            for kw in range(self.kernel_size):
                                ih = oh * self.stride + kh
                                iw = ow * self.stride + kw
                                val += (x.get(ic, ih, iw) *
                                       self.filter.weights[oc][ic][kh][kw])
                    output.set(oc, oh, ow, val + self.filter.bias[oc])

        return output

    def _pad(self, x: Tensor3D, p: int) -> Tensor3D:
        """제로 패딩"""
        new_h = x.height + 2 * p
        new_w = x.width + 2 * p
        padded = Tensor3D.zeros(x.channels, new_h, new_w)
        for c in range(x.channels):
            for h in range(x.height):
                for w in range(x.width):
                    padded.set(c, h + p, w + p, x.get(c, h, w))
        return padded


class MaxPool2D:
    """최대 풀링 레이어"""

    def __init__(self, kernel_size: int = 2, stride: int = 2):
        self.kernel_size = kernel_size
        self.stride = stride

    def forward(self, x: Tensor3D) -> Tensor3D:
        out_h = (x.height - self.kernel_size) // self.stride + 1
        out_w = (x.width - self.kernel_size) // self.stride + 1

        output = Tensor3D.zeros(x.channels, out_h, out_w)

        for c in range(x.channels):
            for oh in range(out_h):
                for ow in range(out_w):
                    max_val = float('-inf')
                    for kh in range(self.kernel_size):
                        for kw in range(self.kernel_size):
                            ih = oh * self.stride + kh
                            iw = ow * self.stride + kw
                            val = x.get(c, ih, iw)
                            if val > max_val:
                                max_val = val
                    output.set(c, oh, ow, max_val)

        return output


class BatchNorm2D:
    """배치 정규화 (단순화: 추론 모드만)"""

    def __init__(self, num_features: int, eps: float = 1e-5):
        self.num_features = num_features
        self.eps = eps
        # 학습된 파라미터 (시뮬레이션)
        self.gamma = [1.0] * num_features
        self.beta = [0.0] * num_features
        self.running_mean = [0.0] * num_features
        self.running_var = [1.0] * num_features

    def forward(self, x: Tensor3D) -> Tensor3D:
        output = Tensor3D.zeros(x.channels, x.height, x.width)

        for c in range(x.channels):
            mean = self.running_mean[c]
            var = self.running_var[c]
            std = math.sqrt(var + self.eps)

            for h in range(x.height):
                for w in range(x.width):
                    normalized = (x.get(c, h, w) - mean) / std
                    output.set(c, h, w,
                              self.gamma[c] * normalized + self.beta[c])

        return output


class ReLU:
    """ReLU 활성화"""

    def forward(self, x: Tensor3D) -> Tensor3D:
        output = Tensor3D.zeros(x.channels, x.height, x.width)
        for c in range(x.channels):
            for h in range(x.height):
                for w in range(x.width):
                    output.set(c, h, w, relu(x.get(c, h, w)))
        return output


class Flatten:
    """평탄화"""

    def forward(self, x: Tensor3D) -> List[float]:
        flat = []
        for c in range(x.channels):
            for h in range(x.height):
                for w in range(x.width):
                    flat.append(x.get(c, h, w))
        return flat


class Linear:
    """완전연결 레이어"""

    def __init__(self, in_features: int, out_features: int):
        self.in_features = in_features
        self.out_features = out_features
        scale = math.sqrt(2.0 / in_features)
        self.weights = [[random.gauss(0, scale) for _ in range(out_features)]
                        for _ in range(in_features)]
        self.bias = [0.0] * out_features

    def forward(self, x: List[float]) -> List[float]:
        output = []
        for j in range(self.out_features):
            val = self.bias[j]
            for i in range(self.in_features):
                val += x[i] * self.weights[i][j]
            output.append(val)
        return output


class ResidualBlock:
    """ResNet 잔차 블록"""

    def __init__(self, channels: int):
        self.conv1 = Conv2D(channels, channels, 3, padding=1)
        self.bn1 = BatchNorm2D(channels)
        self.relu = ReLU()
        self.conv2 = Conv2D(channels, channels, 3, padding=1)
        self.bn2 = BatchNorm2D(channels)

    def forward(self, x: Tensor3D) -> Tensor3D:
        identity = x

        out = self.conv1.forward(x)
        out = self.bn1.forward(out)
        out = self.relu.forward(out)

        out = self.conv2.forward(out)
        out = self.bn2.forward(out)

        # Skip Connection
        for c in range(out.channels):
            for h in range(out.height):
                for w in range(out.width):
                    out.set(c, h, w, out.get(c, h, w) + identity.get(c, h, w))

        out = self.relu.forward(out)
        return out


class SimpleCNN:
    """간단한 CNN 모델"""

    def __init__(self, num_classes: int = 10):
        # 특징 추출부
        self.conv1 = Conv2D(3, 32, 3, padding=1)
        self.bn1 = BatchNorm2D(32)
        self.relu = ReLU()
        self.pool1 = MaxPool2D(2)

        self.conv2 = Conv2D(32, 64, 3, padding=1)
        self.bn2 = BatchNorm2D(64)
        self.pool2 = MaxPool2D(2)

        self.conv3 = Conv2D(64, 128, 3, padding=1)
        self.bn3 = BatchNorm2D(128)
        self.pool3 = MaxPool2D(2)

        self.flatten = Flatten()

        # 분류부
        self.fc1 = Linear(128 * 4 * 4, 256)  # 32x32 → 4x4 after 3 pools
        self.fc2 = Linear(256, num_classes)

    def forward(self, x: Tensor3D) -> List[float]:
        # Conv Block 1
        x = self.conv1.forward(x)
        x = self.bn1.forward(x)
        x = self.relu.forward(x)
        x = self.pool1.forward(x)

        # Conv Block 2
        x = self.conv2.forward(x)
        x = self.bn2.forward(x)
        x = self.relu.forward(x)
        x = self.pool2.forward(x)

        # Conv Block 3
        x = self.conv3.forward(x)
        x = self.bn3.forward(x)
        x = self.relu.forward(x)
        x = self.pool3.forward(x)

        # Flatten & FC
        flat = self.flatten.forward(x)
        fc1_out = [relu(v) for v in self.fc1.forward(flat)]
        logits = self.fc2.forward(fc1_out)

        return logits

    def predict(self, x: Tensor3D) -> int:
        """클래스 예측"""
        logits = self.forward(x)
        return logits.index(max(logits))

    def predict_proba(self, x: Tensor3D) -> List[float]:
        """클래스 확률"""
        logits = self.forward(x)
        return softmax(logits)


class YOLOv8Detector:
    """YOLO 스타일 객체 탐지기 (시뮬레이션)"""

    def __init__(self, num_classes: int = 80):
        self.num_classes = num_classes
        self.anchor_boxes = [
            (10, 13), (16, 30), (33, 23),
            (30, 61), (62, 45), (59, 119),
            (116, 90), (156, 198), (373, 326)
        ]

    def detect(self, image: Tensor3D,
               confidence_threshold: float = 0.5) -> List[dict]:
        """객체 탐지 (시뮬레이션)"""
        # 실제로는 CNN 백본 + 헤드 통과
        detections = []

        # 시뮬레이션: 랜덤 박스 생성
        num_detections = random.randint(0, 5)
        for _ in range(num_detections):
            conf = random.random()
            if conf > confidence_threshold:
                detections.append({
                    'bbox': [
                        random.randint(0, image.width - 100),
                        random.randint(0, image.height - 100),
                        random.randint(50, 150),
                        random.randint(50, 150)
                    ],
                    'confidence': conf,
                    'class': random.randint(0, self.num_classes - 1)
                })

        # NMS (Non-Maximum Suppression) 시뮬레이션
        detections = self._nms(detections)
        return detections

    def _nms(self, detections: List[dict], iou_threshold: float = 0.5) -> List[dict]:
        """Non-Maximum Suppression"""
        if not detections:
            return []

        # 신뢰도 기준 정렬
        detections.sort(key=lambda x: x['confidence'], reverse=True)

        keep = []
        while detections:
            best = detections.pop(0)
            keep.append(best)

            # IoU 계산 후 제거
            remaining = []
            for det in detections:
                iou = self._compute_iou(best['bbox'], det['bbox'])
                if iou < iou_threshold:
                    remaining.append(det)
            detections = remaining

        return keep

    def _compute_iou(self, box1: List[int], box2: List[int]) -> float:
        """IoU 계산"""
        x1, y1, w1, h1 = box1
        x2, y2, w2, h2 = box2

        # 교집합
        xi1 = max(x1, x2)
        yi1 = max(y1, y2)
        xi2 = min(x1 + w1, x2 + w2)
        yi2 = min(y1 + h1, y2 + h2)

        if xi2 <= xi1 or yi2 <= yi1:
            return 0.0

        inter = (xi2 - xi1) * (yi2 - yi1)
        union = w1 * h1 + w2 * h2 - inter

        return inter / union if union > 0 else 0.0


# ============================================================
# 사용 예시
# ============================================================

if __name__ == "__main__":
    print("=" * 60)
    print("         CNN (합성곱 신경망) 구현 데모")
    print("=" * 60)

    # 1. 기본 합성곱 연산
    print("\n1. 합성곱 연산")
    print("-" * 40)

    # 32×32×3 입력 이미지 생성
    input_image = Tensor3D.random(3, 32, 32)
    print(f"입력: {input_image.channels}×{input_image.height}×{input_image.width}")

    # Conv 레이어
    conv = Conv2D(3, 32, 3, padding=1)
    conv_out = conv.forward(input_image)
    print(f"Conv(3→32, 3×3) 출력: {conv_out.channels}×{conv_out.height}×{conv_out.width}")

    # ReLU
    relu_layer = ReLU()
    relu_out = relu_layer.forward(conv_out)
    print(f"ReLU 출력: {relu_out.channels}×{relu_out.height}×{relu_out.width}")

    # Max Pooling
    pool = MaxPool2D(2)
    pool_out = pool.forward(relu_out)
    print(f"MaxPool(2×2) 출력: {pool_out.channels}×{pool_out.height}×{pool_out.width}")

    # 2. 전체 CNN 모델
    print("\n\n2. 전체 CNN 분류 모델")
    print("-" * 40)

    model = SimpleCNN(num_classes=10)

    # 32×32 RGB 이미지 입력
    test_image = Tensor3D.random(3, 32, 32)
    print(f"입력 이미지: {test_image.height}×{test_image.width}×{test_image.channels}")

    # 예측
    logits = model.forward(test_image)
    probs = softmax(logits)
    predicted_class = logits.index(max(logits))

    print(f"로짓: {[f'{l:.2f}' for l in logits[:5]]}...")
    print(f"확률: {[f'{p:.3f}' for p in probs[:5]]}...")
    print(f"예측 클래스: {predicted_class} (신뢰도: {probs[predicted_class]:.2%})")

    # 3. ResNet 스타일 Skip Connection
    print("\n\n3. ResNet 잔차 블록")
    print("-" * 40)

    res_block = ResidualBlock(64)
    res_input = Tensor3D.random(64, 16, 16)
    res_output = res_block.forward(res_input)

    print(f"입력: {res_input.channels}×{res_input.height}×{res_input.width}")
    print(f"출력: {res_output.channels}×{res_output.height}×{res_output.width}")
    print("Skip Connection: F(x) + x 형태로 정보 보존")

    # 4. YOLO 객체 탐지 시뮬레이션
    print("\n\n4. YOLO 객체 탐지 시뮬레이션")
    print("-" * 40)

    detector = YOLOv8Detector(num_classes=80)
    test_img = Tensor3D.random(3, 416, 416)

    detections = detector.detect(test_img, confidence_threshold=0.5)
    print(f"탐지된 객체: {len(detections)}개")

    for i, det in enumerate(detections):
        bbox, conf, cls = det['bbox'], det['confidence'], det['class']
        print(f"  객체 {i+1}: bbox={bbox}, conf={conf:.2f}, class={cls}")

    print("\n" + "=" * 60)
    print("CNN의 핵심:")
    print("1. 합성곱: 공간적 특징 추출 (가중치 공유)")
    print("2. 풀링: 공간 축소 + 이동 불변성")
    print("3. Skip Connection: 깊은 네트워크 학습 가능")
    print("=" * 60)