CNN의 일반적인 구조 (Conv+ReLU → Pooling → FC → Softmax)를 설명할 수 있다
ReLU 활성화 , 풀링 층 , 완전연결 층 의 역할을 이해한다
CNN을 통과하며 텐서 크기가 어떻게 변하는지 직접 계산할 수 있다
가중치 공유 와 이동 불변성 이 CNN의 핵심 장점임을 설명할 수 있다
대표 CNN 아키텍처의 세 가지 핵심 아이디어 (깊게·작게, 잔차 연결, 효율)를 설명하고, 전이학습에서 모델을 고를 때 참고할 수 있다
CNN의 일반적인 구조
이전 챕터와의 연결
Ch06에서 콘볼루션 연산의 원리를 배웠다: 커널이 이미지를 훑으며 특성 맵을 만드는 과정이다. 이제 이 연산을 여러 층으로 쌓아 하나의 완전한 신경망을 구성하는 방법을 살펴보자.
CNN 파이프라인
CNN은 크게 특성 학습(Feature Learning) 단계와 분류(Classification) 단계로 나뉜다.
입력 이미지 Conv + ReLU 특성 추출 Pooling 크기 축소 반복 ⋯ Conv+Pool Flatten 1차원 변환 FC 완전연결 Softmax 분류 출력 Feature Learning Classification
각 구성 요소를 하나씩 살펴보자.
ReLU 활성화
왜 활성화 함수가 필요한가?
콘볼루션은 선형 연산 (원소별 곱의 합)이다. 선형 연산만 반복하면 아무리 층을 쌓아도 결국 하나의 선형 변환과 같다. 비선형 활성화 함수 를 삽입해야 네트워크가 복잡한 패턴을 학습할 수 있다.
\[f(u) = \max(0,\, u)\]
콘볼루션으로 추출한 특성 맵에서 양의 값만 활성화 시키고, 음의 값은 0으로 만드는 함수이다.
ReLU의 효과
특성 맵에 ReLU를 적용하면 두드러지는 특징만 강조 되고 배경 잡음이 제거된다.
예를 들어 콘볼루션 결과가 다음과 같다면:
\[\begin{bmatrix} -5 & 3 & 7 \\ 2 & -3 & 9 \\ 1 & 1 & -1 \end{bmatrix}
\xrightarrow{\text{ReLU}}
\begin{bmatrix} 0 & 3 & 7 \\ 2 & 0 & 9 \\ 1 & 1 & 0 \end{bmatrix}\]
음수값(-5, -3, -1)이 모두 0으로 바뀌어, 해당 커널이 감지한 특성이 존재하는 부분 만 남게 된다.
풀링 층 (Pooling Layer)
풀링이란?
입력 특성 맵의 공간 크기를 줄이는 다운샘플링 연산이다.
풀링 크기 : 윈도우 크기 (보통 \(2 \times 2\) )
스트라이드 : 윈도우 이동 간격 (보통 2)
방식 : Max Pooling(최댓값) 또는 Average Pooling(평균값)
Max Pooling 예시
\(4 \times 4\) 입력에 \(2 \times 2\) Max Pooling (스트라이드 2)을 적용하면:
\[\begin{bmatrix} 1 & 2 & 1 & 0 \\ 0 & 1 & 2 & 3 \\ 3 & 0 & 1 & 2 \\ 2 & 4 & 0 & 1 \end{bmatrix}
\xrightarrow{2 \times 2 \;\text{Max Pool}}
\begin{bmatrix} 2 & 3 \\ 4 & 2 \end{bmatrix}\]
각 \(2 \times 2\) 영역에서 최댓값 만 선택한다.
왜 풀링을 쓰는가?
크기 축소 : 이미지 크기를 줄이되 핵심 특성은 보존한다
연산 효율 : 다음 층의 계산량을 줄인다
분산 감소 : 축약 과정에서 variance를 줄이는 효과가 있다
풀링은 각 채널에 독립적으로 적용된다. 3채널 입력이면 3채널 출력, 채널 수는 변하지 않는다.
완전연결 층 (Fully Connected Layer)
CNN의 마지막 단계에서는 특성 맵을 1차원 벡터로 펼쳐서(Flatten) 완전연결 층에 입력한다.
예를 들어, 마지막 풀링 결과가 \(3 \times 3\) 크기의 특성 맵이라면:
\[\begin{bmatrix} 1 & 0 & 2 \\ 4 & 6 & 1 \\ 3 & 1 & 5 \end{bmatrix}
\xrightarrow{\text{Flatten}}
\begin{bmatrix} 1 & 0 & 2 & 4 & 6 & 1 & 3 & 1 & 5 \end{bmatrix}\]
이 벡터가 Ch01~Ch03에서 배운 일반 신경망의 입력이 되어, 최종 분류를 수행한다.
CNN 연산 연습
예제 1: 39 x 39 x 3 이미지
다음 3층 CNN의 각 단계별 출력 크기를 추적해 보자.
입력
—
—
—
—
39 × 39 × 3
Conv1
3
1
0
10
37 × 37 × 10
Conv2
5
2
0
20
17 × 17 × 20
Conv3
5
2
0
40
7 × 7 × 40
Flatten
—
—
—
—
1960
계산 과정 (Ch06의 출력 크기 공식 \(\lfloor(N - K + 2P) / S\rfloor + 1\) 적용):
Conv1: \(\lfloor(39 - 3 + 0) / 1\rfloor + 1 = 37\) , 커널 10개 → \(37 \times 37 \times 10\)
Conv2: \(\lfloor(37 - 5 + 0) / 2\rfloor + 1 = 17\) , 커널 20개 → \(17 \times 17 \times 20\)
Conv3: \(\lfloor(17 - 5 + 0) / 2\rfloor + 1 = 7\) , 커널 40개 → \(7 \times 7 \times 40\)
Flatten: \(7 \times 7 \times 40 = 1{,}960\)
각 커널의 차원도 확인하자:
Conv1 커널 1개: \(3 \times 3 \times 3\) (입력 채널 수 = 3)
Conv2 커널 1개: \(5 \times 5 \times 10\) (입력 채널 수 = 10)
Conv3 커널 1개: \(5 \times 5 \times 20\) (입력 채널 수 = 20)
크기를 추적했으니 파라미터 수 도 세어 보자 (Ch06의 공식: 커널 1개 = \(K \times K \times C + 1\) ):
Conv1
\(3 \times 3 \times 3 + 1 = 28\)
10
\(280\)
Conv2
\(5 \times 5 \times 10 + 1 = 251\)
20
\(5{,}020\)
Conv3
\(5 \times 5 \times 20 + 1 = 501\)
40
\(20{,}040\)
특성 추출부 합계
\(25{,}340\)
\(39 \times 39 \times 3\) 이미지를 3층으로 처리하는 데 3만 개가 안 되는 파라미터면 충분하다. 같은 이미지를 FC로 펼쳐 은닉 256 노드에 연결하기만 해도 약 117만 개가 필요하다는 것과 비교해 보라.
예제 2: LeNet-5 스타일 (32 x 32 x 3)
입력
—
—
—
—
32 × 32 × 3
Conv1
5
1
0
6
28 × 28 × 6
Pool1
2
2
—
—
14 × 14 × 6
Conv2
5
1
0
10
10 × 10 × 10
Pool2
2
2
—
—
5 × 5 × 10
Flatten
—
—
—
—
250
FC3
—
—
—
—
120
FC4
—
—
—
—
84
풀링 층에서는 공간 크기가 절반 으로 줄어들고, 채널 수는 유지 된다.
인터랙티브 — CNN 파이프라인 계산기
입력 크기와 각 층의 파라미터를 조정하면 텐서 크기가 어떻게 변하는지 실시간으로 확인할 수 있습니다.
viewof cnn_input = Inputs. range ([8 , 128 ], {value : 32 , step : 1 , label : "입력 크기 (N×N)" })
viewof cnn_ch = Inputs. range ([1 , 3 ], {value : 3 , step : 1 , label : "입력 채널 수" })
viewof k1 = Inputs. range ([1 , 7 ], {value : 5 , step : 2 , label : "Conv1 커널 크기" })
viewof f1 = Inputs. range ([1 , 32 ], {value : 6 , step : 1 , label : "Conv1 커널 수" })
viewof pool1_size = Inputs. range ([1 , 4 ], {value : 2 , step : 1 , label : "Pool1 크기" })
viewof k2 = Inputs. range ([1 , 7 ], {value : 5 , step : 2 , label : "Conv2 커널 크기" })
viewof f2 = Inputs. range ([1 , 64 ], {value : 16 , step : 1 , label : "Conv2 커널 수" })
viewof pool2_size = Inputs. range ([1 , 4 ], {value : 2 , step : 1 , label : "Pool2 크기" })
cnn_calc = {
const n0 = cnn_input;
const c0 = cnn_ch;
// Conv1 (stride=1, padding=0)
const n1 = Math . floor ((n0 - k1) / 1 ) + 1 ;
const c1 = f1;
const valid1 = n1 > 0 ;
// Pool1
const n2 = valid1 ? Math . floor (n1 / pool1_size) : 0 ;
const c2 = c1;
const valid2 = n2 > 0 ;
// Conv2 (stride=1, padding=0)
const n3 = valid2 ? Math . floor ((n2 - k2) / 1 ) + 1 : 0 ;
const c3 = f2;
const valid3 = n3 > 0 ;
// Pool2
const n4 = valid3 ? Math . floor (n3 / pool2_size) : 0 ;
const c4 = c3;
const valid4 = n4 > 0 ;
// Flatten
const flat = valid4 ? n4 * n4 * c4 : 0 ;
return {n0, c0, n1, c1, n2, c2, n3, c3, n4, c4, flat,
valid1, valid2, valid3, valid4};
}
html `<svg viewBox="0 0 820 260" xmlns="http://www.w3.org/2000/svg" style="width:100%;max-width:820px;display:block;margin:0 auto;font-family:sans-serif;">
<defs>
<marker id="ahc" markerWidth="7" markerHeight="5" refX="7" refY="2.5" orient="auto"><path d="M0,0 L7,2.5 L0,5 Z" fill="#888"/></marker>
</defs>
<!-- Input -->
<rect x="5" y="60" width="100" height="70" rx="8" fill="#8C1515" opacity="0.12" stroke="#8C1515" stroke-width="1.5"/>
<text x="55" y="85" text-anchor="middle" font-size="12" font-weight="700" fill="#8C1515">입력</text>
<text x="55" y="105" text-anchor="middle" font-size="11" fill="#333"> ${ cnn_calc. n0 } × ${ cnn_calc. n0 } × ${ cnn_calc. c0 } </text>
<text x="55" y="122" text-anchor="middle" font-size="9" fill="#888"> ${ cnn_calc. n0 * cnn_calc. n0 * cnn_calc. c0 } values</text>
<line x1="105" y1="95" x2="128" y2="95" stroke="#888" stroke-width="1" marker-end="url(#ahc)"/>
<!-- Conv1 -->
<rect x="130" y="50" width="100" height="90" rx="8" fill=" ${ cnn_calc. valid1 ? '#1F3F7A' : '#cc0000' } " opacity="0.12" stroke=" ${ cnn_calc. valid1 ? '#1F3F7A' : '#cc0000' } " stroke-width="1.5"/>
<text x="180" y="75" text-anchor="middle" font-size="12" font-weight="700" fill="#1F3F7A">Conv1+ReLU</text>
<text x="180" y="95" text-anchor="middle" font-size="10" fill="#555"> ${ k1} × ${ k1} , ${ f1} 개</text>
<text x="180" y="115" text-anchor="middle" font-size="11" fill=" ${ cnn_calc. valid1 ? '#333' : '#cc0000' } "> ${ cnn_calc. valid1 ? cnn_calc. n1 + '×' + cnn_calc. n1 + '×' + cnn_calc. c1 : '오류!' } </text>
<text x="180" y="132" text-anchor="middle" font-size="9" fill="#888"> ${ cnn_calc. valid1 ? 'kernel: ' + k1+ '×' + k1+ '×' + cnn_calc. c0 : '' } </text>
<line x1="230" y1="95" x2="253" y2="95" stroke="#888" stroke-width="1" marker-end="url(#ahc)"/>
<!-- Pool1 -->
<rect x="255" y="60" width="90" height="70" rx="8" fill="#D4A017" opacity="0.15" stroke="#D4A017" stroke-width="1.5"/>
<text x="300" y="85" text-anchor="middle" font-size="12" font-weight="700" fill="#9A7B00">Pool1</text>
<text x="300" y="100" text-anchor="middle" font-size="10" fill="#555"> ${ pool1_size} × ${ pool1_size} </text>
<text x="300" y="118" text-anchor="middle" font-size="11" fill=" ${ cnn_calc. valid2 ? '#333' : '#cc0000' } "> ${ cnn_calc. valid2 ? cnn_calc. n2 + '×' + cnn_calc. n2 + '×' + cnn_calc. c2 : '—' } </text>
<line x1="345" y1="95" x2="368" y2="95" stroke="#888" stroke-width="1" marker-end="url(#ahc)"/>
<!-- Conv2 -->
<rect x="370" y="50" width="100" height="90" rx="8" fill=" ${ cnn_calc. valid3 ? '#1F3F7A' : '#cc0000' } " opacity="0.12" stroke=" ${ cnn_calc. valid3 ? '#1F3F7A' : '#cc0000' } " stroke-width="1.5"/>
<text x="420" y="75" text-anchor="middle" font-size="12" font-weight="700" fill="#1F3F7A">Conv2+ReLU</text>
<text x="420" y="95" text-anchor="middle" font-size="10" fill="#555"> ${ k2} × ${ k2} , ${ f2} 개</text>
<text x="420" y="115" text-anchor="middle" font-size="11" fill=" ${ cnn_calc. valid3 ? '#333' : '#cc0000' } "> ${ cnn_calc. valid3 ? cnn_calc. n3 + '×' + cnn_calc. n3 + '×' + cnn_calc. c3 : '오류!' } </text>
<text x="420" y="132" text-anchor="middle" font-size="9" fill="#888"> ${ cnn_calc. valid3 ? 'kernel: ' + k2+ '×' + k2+ '×' + cnn_calc. c2 : '' } </text>
<line x1="470" y1="95" x2="493" y2="95" stroke="#888" stroke-width="1" marker-end="url(#ahc)"/>
<!-- Pool2 -->
<rect x="495" y="60" width="90" height="70" rx="8" fill="#D4A017" opacity="0.15" stroke="#D4A017" stroke-width="1.5"/>
<text x="540" y="85" text-anchor="middle" font-size="12" font-weight="700" fill="#9A7B00">Pool2</text>
<text x="540" y="100" text-anchor="middle" font-size="10" fill="#555"> ${ pool2_size} × ${ pool2_size} </text>
<text x="540" y="118" text-anchor="middle" font-size="11" fill=" ${ cnn_calc. valid4 ? '#333' : '#cc0000' } "> ${ cnn_calc. valid4 ? cnn_calc. n4 + '×' + cnn_calc. n4 + '×' + cnn_calc. c4 : '—' } </text>
<line x1="585" y1="95" x2="608" y2="95" stroke="#888" stroke-width="1" marker-end="url(#ahc)"/>
<!-- Flatten + FC -->
<rect x="610" y="55" width="90" height="80" rx="8" fill="#8C1515" opacity="0.08" stroke="#8C1515" stroke-width="1"/>
<text x="655" y="80" text-anchor="middle" font-size="12" font-weight="700" fill="#8C1515">Flatten</text>
<text x="655" y="100" text-anchor="middle" font-size="13" font-weight="700" fill=" ${ cnn_calc. valid4 ? '#333' : '#cc0000' } "> ${ cnn_calc. valid4 ? cnn_calc. flat . toLocaleString () : '—' } </text>
<text x="655" y="118" text-anchor="middle" font-size="9" fill="#888"> ${ cnn_calc. valid4 ? cnn_calc. n4 + '×' + cnn_calc. n4 + '×' + cnn_calc. c4 : '' } </text>
<line x1="700" y1="95" x2="723" y2="95" stroke="#888" stroke-width="1" marker-end="url(#ahc)"/>
<!-- Softmax -->
<rect x="725" y="65" width="80" height="60" rx="8" fill="#D4A017" opacity="0.2" stroke="#D4A017" stroke-width="1.5"/>
<text x="765" y="90" text-anchor="middle" font-size="12" font-weight="700" fill="#9A7B00">Softmax</text>
<text x="765" y="108" text-anchor="middle" font-size="9" fill="#555">분류</text>
<!-- Labels -->
<text x="300" y="220" text-anchor="middle" font-size="13" font-weight="700" fill="#1F3F7A">Feature Learning</text>
<line x1="130" y1="205" x2="470" y2="205" stroke="#1F3F7A" stroke-width="2" opacity="0.4"/>
<text x="655" y="220" text-anchor="middle" font-size="13" font-weight="700" fill="#8C1515">Classification</text>
<line x1="610" y1="205" x2="805" y2="205" stroke="#8C1515" stroke-width="2" opacity="0.4"/>
</svg>`
콘볼루션의 핵심 특징
가중치 공유 (Weight Sharing)
Ch06에서 가중치 공유가 파라미터를 어떻게 절약하는지 이미 확인했다. 여기서는 네트워크 구조의 관점 에서 한 번 더 정리한다 — 콘볼루션 연산에서 하나의 커널은 이미지의 모든 위치에 동일하게 적용 된다. 이것이 가중치 공유(Weight Sharing) 이다.
5 × 5 이미지에 3 × 3 커널을 적용하면 9개의 출력값이 만들어진다.
완전연결 : 25개 입력 × 9개 출력 → \((25+1) \times 9 = 234\) 개 파라미터
콘볼루션 : 커널 가중치 + 편향 → \(9 + 1 = 10\) 개 파라미터
같은 커널(가중치)을 이미지의 모든 위치에 공유 적용 하여, 파라미터 수를 크게 줄이면서도 동일한 특성을 이미지 전체에서 감지할 수 있는 특성이다.
이동 불변성 (Shift Invariance)
가중치 공유의 자연스러운 결과로, CNN은 이동 불변성 을 갖는다.
이미지에서 특정 특성(예: 대각선 패턴)이 어디에 위치하든 같은 커널이 감지한다. 사람이 이미지를 인식할 때도 특성의 존재 여부 가 중요하지, 특성이 정확히 어느 위치 에 있는지는 크게 중요하지 않다.
예를 들어 고양이가 이미지의 왼쪽 위에 있든, 오른쪽 아래에 있든 “고양이”로 분류할 수 있다. 이것이 콘볼루션 + 풀링의 조합이 주는 이동 불변성(Translation Invariance) 이다.
CNN 아키텍처: 계보와 세 가지 아이디어
딥러닝 아키텍처(Architecture)란 네트워크를 구성하는 특정한 패턴 이다. 1998년부터 “더 정확한 구조”를 찾는 경쟁이 이어져 왔지만, 이 절의 목표는 그 역사를 암기하는 것이 아니다 — Ch08의 전이학습에서 사전 학습 모델을 고를 때 만나게 될 이름들 이 각각 어떤 아이디어인지 파악하는 것이다.
시작하기 전에: 여러분은 이미 아키텍처 하나를 손으로 계산했다. 위의 예제 2가 바로 최초의 실용 CNN인 LeNet-5 (1998, 손글씨 인식)의 축소판이다. 2012년에는 AlexNet 이 ReLU와 GPU 학습을 도입해 ImageNet 대회를 제패하며 딥러닝 시대를 열었다. 그 이후의 발전은 세 가지 아이디어로 요약된다.
아이디어 1 — 더 깊게, 더 작은 커널로 (VGG-16, 2014)
모든 콘볼루션에 3×3 커널만 사용하되, 층을 16개로 쌓았다. 설계 패턴이 규칙적이다: 블록을 지날 때마다 공간 크기는 절반, 채널 수는 2배 (\(224 \to 112 \to 56 \to \cdots \to 7\) , 채널 \(64 \to 128 \to \cdots \to 512\) ). 구조가 단순한 만큼 지금도 교육과 실무의 기준점으로 쓰인다. 약 1억 3,800만 파라미터.
아이디어 2 — 우회로를 놓아라 (ResNet, 2015)
층을 깊게 쌓으면 정확도가 올라가지만, 한계에 부딪힌다: 기울기 소실 — Ch04에서 손으로 확인했던, 역전파의 미분이 층마다 곱해지며 사라지는 그 문제다. 이 한계를 돌파한 것이 ResNet의 잔차 연결 이다.
입력 \(\mathbf{x}\) 가 층들을 통과한 결과 \(F(\mathbf{x})\) 에 원래 입력을 더하여 다음 층에 전달하는 구조:
\[\text{출력} = F(\mathbf{x}) + \mathbf{x}\]
이를 잔차 블록(Residual Block) 이라 한다.
기존 구조 x Layer i F Layer i+n F(x) 잔차 연결 x Layer i F Layer i+n identity (x) + F(x) + x Residual Block
우회로(identity 경로)로는 미분이 곱셈 없이 그대로 통과하므로, 기울기가 소실되지 않고 깊은 층까지 전달 된다. ResNet은 이 구조로 152층 까지 쌓아 ImageNet 1위를 달성했고, 지금도 전이학습에서 가장 흔히 쓰이는 기본 모델이다 (HW2에서 만나게 된다).
아이디어 3 — 필요한 계산만 하라 (Inception, 2014)
“어떤 크기의 커널이 최적인가?”라는 질문에, Inception은 여러 크기의 커널을 병렬로 적용 하고 결과를 합치는 방식으로 답했다. 여기에 \(1 \times 1\) 콘볼루션으로 채널을 먼저 줄여 연산량을 약 1/10로 절감하는 병목(bottleneck) 구조를 더했다. 세부 계산은 이 수업의 범위 밖이다 — “같은 성능을 훨씬 적은 계산으로”라는 방향만 기억하면 된다.
계보 한눈에 보기
LeNet-5
1998
32×32
~6만
Conv-Pool 반복 — 최초의 실용 CNN (예제 2가 이 구조)
AlexNet
2012
227×227
~6천만
ReLU, Dropout, GPU 학습 — 딥러닝 시대 개막
VGG-16
2014
224×224
~1.4억
3×3 커널만, 깊게 (아이디어 1)
ResNet
2015
224×224
~2,500만
잔차 연결 (아이디어 2) — 전이학습의 기본 선택지
Inception
2014
224×224
~500만
병렬 커널 + 1×1 병목 (아이디어 3)
문제집과 시험에서 다루는 것은 이 장 전반부의 크기·파라미터 계산 과, “잔차 연결이 왜 기울기 소실을 완화하는가”의 개념 까지다. 각 아키텍처의 층 구성을 외울 필요는 없다 — 실무에서도 표를 찾아보지, 외우지 않는다.
정리
이 챕터에서 학습한 내용을 요약하면:
CNN 구조
Conv+ReLU → Pooling → ⋯ → Flatten → FC → Softmax
ReLU
\(\max(0, u)\) 로 비선형성 추가, 양의 특성만 활성화
풀링
공간 크기 축소, 특성 보존, 채널 수 불변
완전연결 층
Flatten 후 분류 수행
가중치 공유
동일 커널을 전체 이미지에 적용 → 파라미터 대폭 절감
이동 불변성
특성의 위치에 무관하게 감지 가능
아키텍처 계보
더 깊게·작게(VGG) → 우회로(ResNet) → 효율(Inception) — 전이학습에서 만날 이름들
ResNet 잔차 연결
\(F(\mathbf{x}) + \mathbf{x}\) 우회로로 기울기 소실 완화 → 초심층 학습 가능
다음 챕터 에서는 학습한 CNN의 정확도를 높이는 전략 (데이터 증강, 전이 학습)과 이미지 분류를 넘어선 컴퓨터 비전 응용 (객체 탐지, 시맨틱 분할)을 다룬다.