Deep Learning/cs231n

cs231n Lecture 4 - Backpropagation and Neural Networks

준성(JunSeong) 2026. 6. 2. 12:39
Stanford cs231n: Deep Learning for Computer Vision 강의 정리 시리즈
챕터 4 - Backpropagation and Neural Networks
#cs231n#딥러닝#Backpropagation#역전파#ComputationalGraph#NeuralNetwork

1. 지금까지의 흐름

3강까지 배운 걸 한 줄로 요약하면 이렇다. score function으로 클래스 점수를 계산하고, loss function으로 W가 얼마나 나쁜지를 수치화하고, gradient descent로 loss를 줄이는 방향으로 W를 업데이트한다.

이제 남은 문제는 복잡한 네트워크에서 gradient를 어떻게 효율적으로 계산하냐는 것이다. Linear Classifier 하나짜리는 직접 미분할 수 있었는데, AlexNet처럼 수백만 개의 파라미터가 있는 네트워크는 그게 불가능하다. 그 답이 Backpropagation이다.


2. Computational Graph

복잡한 함수의 gradient를 체계적으로 계산하기 위해 Computational Graph(연산 그래프)를 사용한다. 어떤 복잡한 수식이든 작은 연산 노드들의 연결로 쪼개서 표현하는 방식이다.

이 표현 방식의 진짜 강점은 각 노드가 전체 네트워크 구조를 몰라도 된다는 점이다. 노드는 자신의 입출력만 보고 local gradient를 계산하면 되고, 나머지는 chain rule로 자동으로 연결된다. CNN이든 RNN이든 아무리 복잡한 네트워크도 결국 이 방식으로 gradient를 계산한다.

그리고 노드를 얼마나 잘게 쪼갤지는 개발자가 결정한다. 예를 들어 sigmoid 함수를 exp, 덧셈, 나눗셈으로 쪼갤 수도 있고, 아예 sigmoid 하나의 노드로 묶을 수도 있다. 어느 쪽이든 수학적으로 동일하지만 하나로 묶으면 더 효율적이다. 이게 sigmoid gate 얘기다.


3. Backpropagation & Chain Rule

Backpropagation은 Chain Rule을 computational graph 위에서 뒤에서 앞으로 재귀적으로 적용하는 것이다.

Chain Rule은 합성함수 미분이다. \(f(g(x))\)를 \(x\)로 미분하면:

$$\frac{\partial f}{\partial x} = \frac{\partial f}{\partial g} \cdot \frac{\partial g}{\partial x}$$

각 노드에서의 gradient 계산은 이렇다:

$$\frac{\partial L}{\partial x} = \underbrace{\frac{\partial z}{\partial x}}_{\text{local gradient}} \times \underbrace{\frac{\partial L}{\partial z}}_{\text{upstream gradient}}$$
Local gradient 개념

▲ 각 노드는 local gradient(∂z/∂x, ∂z/∂y)를 갖고, output 방향에서 upstream gradient를 받음 (출처: cs231n Lecture 4)

Chain rule 적용

▲ ∂L/∂x = ∂L/∂z · ∂z/∂x - upstream × local gradient를 곱해서 앞으로 전달 (출처: cs231n Lecture 4)

gradient의 직관적인 의미를 짚고 가면, upstream gradient는 "이 값이 최종 Loss에 얼마나 민감하게 영향을 미치는가"를 나타낸다. 어떤 가중치를 조금 올렸을 때 Loss가 얼마나 반응하는지 알려주는 방향 지시등 같은 것이다.

그리고 backward가 항상 \(\frac{\partial f}{\partial f} = 1\)에서 시작하는 건, 최종 출력의 최종 출력에 대한 변화율이 당연히 1:1이기 때문이다. 이 초기값이 없으면 chain rule을 시작할 수가 없다.


4. 계산 예시: (x+y)×z

\(f(x, y, z) = (x + y) \cdot z\)에서 \(x = -2,\ y = 5,\ z = -4\)일 때 직접 계산해보자.

Forward pass: \(q = x + y = 3\), \(f = q \cdot z = -12\)

Backward pass: \(\frac{\partial f}{\partial f} = 1\)에서 시작.

  • \(f = q \cdot z\)이므로: \(\frac{\partial f}{\partial z} = q = 3\), \(\frac{\partial f}{\partial q} = z = -4\)
  • \(q = x + y\)이므로: \(\frac{\partial f}{\partial x} = \frac{\partial f}{\partial q} \cdot 1 = -4\), \(\frac{\partial f}{\partial y} = -4\)
Backpropagation 계산 예시

▲ 초록 = forward 값, 빨강 = backward gradient - 뒤에서 앞으로 흘러옴 (출처: cs231n Lecture 4)

forward pass 때 중간값을 저장해뒀다가 backward에서 쓰는 게 핵심이다. 곱셈 노드에서 \(z\)에 대한 gradient를 계산할 때 forward에서 저장해둔 \(q = 3\)이 필요하다. 이 때문에 forward와 backward를 분리해서 구현하고, forward에서 중간값을 반드시 저장한다.


5. 각 게이트의 gradient 패턴

자주 쓰이는 게이트들의 gradient 패턴을 알아두면 backprop을 빠르게 계산할 수 있다.

게이트 연산 gradient 특징 별명
Add gate \(f = x + y\) upstream gradient를 양쪽 입력에 그대로 나눠줌 gradient distributor
Mul gate \(f = x \cdot y\) \(x\)의 gradient = upstream × \(y\), \(y\)의 gradient = upstream × \(x\) gradient switcher
Max gate \(f = \max(x, y)\) 더 큰 쪽에만 upstream gradient 전달, 작은 쪽은 0 gradient router

Mul gate가 "switcher"인 이유가 직관적인데, \(f = x \cdot y\)를 \(x\)로 미분하면 \(y\)가 남는다. 즉 \(x\)의 gradient를 계산할 때 \(y\) 값이 필요하고, \(y\)의 gradient를 계산할 때 \(x\) 값이 필요하다. 입력값을 서로 교환해서 곱하는 것이다. 여기서 입력값이 아주 크면 반대쪽 gradient가 그만큼 증폭되기 때문에, 데이터 전처리로 스케일을 맞춰주는 게 중요하다.

Patterns in backward flow

▲ add: gradient distributor / max: gradient router - 실제 값과 함께 시각화 (출처: cs231n Lecture 4)


6. Sigmoid 예시

실제 네트워크에서 자주 쓰이는 Sigmoid 함수를 통해 staged computation의 효과를 볼 수 있다.

$$\sigma(x) = \frac{1}{1 + e^{-x}}$$
Sigmoid forward pass

▲ f(w,x) = σ(w₀x₀ + w₁x₁ + w₂) - 여러 노드로 분해된 forward 계산 (출처: cs231n Lecture 4)

이걸 노드 하나하나씩 미분하면 복잡한데, sigmoid 수식을 직접 미분하면 놀랍도록 단순해진다:

$$\frac{d\sigma}{dx} = \sigma(x)(1 - \sigma(x))$$

forward pass에서 sigmoid 출력값 \(\sigma(x)\)를 저장해두면, backward에서 \(\sigma(x)(1 - \sigma(x))\) 한 줄로 끝이다. \(\sigma(x) = 0.73\)이면 gradient는 \(0.73 \times 0.27 \approx 0.2\). 내부 노드들을 하나씩 역전파하는 것보다 훨씬 효율적이다.

Sigmoid backward pass

▲ sigmoid gate 하나로 묶으면 backward가 σ(x)(1-σ(x)) 한 줄로 끝남 (출처: cs231n Lecture 4)

Sigmoid의 단점: Vanishing Gradient

입력이 아주 크거나 작으면 sigmoid 출력이 0이나 1에 가까워지고, gradient \(\sigma(1-\sigma) \approx 0\)이 된다. 이 상태를 saturation이라고 하고, gradient가 거의 0이 되면 그 신호가 앞쪽 레이어로 거의 전달이 안 된다. 이게 Vanishing Gradient 문제고, ReLU가 등장하는 배경이다.


7. 벡터에서의 Backpropagation

실제로는 입출력이 전부 벡터나 행렬이다. 이 경우 gradient는 스칼라가 아니라 Jacobian 행렬이 된다. 입력이 4096차원이면 Jacobian이 4096×4096이라서, 이걸 전부 명시적으로 계산하는 건 비효율적이다. 실제로는 upstream gradient와의 곱을 한 번에 계산한다.

elementwise 연산인 ReLU \(\max(0, x)\)의 경우, 각 입력 원소는 오직 자기 자신에게만 영향을 주기 때문에 Jacobian이 대각 행렬이 된다. 실제 프레임워크는 이 대각선 값들만 추출해서 계산한다. 메모리도 절약되고 계산도 빠르다.

중요한 원칙 하나: 어떤 변수의 gradient는 항상 그 변수와 같은 shape를 가진다. W가 (2×2) 행렬이면 \(\nabla_W f\)도 (2×2)여야 한다. 구현하다가 shape가 안 맞으면 구현이 잘못된 신호다.

Vectorized example q=Wx

▲ f(x,W)=||Wx||² - q에 대한 gradient: ∇qf = 2q (출처: cs231n Lecture 4)

∇Wf = 2q·xᵀ

▲ W에 대한 gradient 계산. shape는 항상 W와 동일해야 함 (출처: cs231n Lecture 4)

∇xf = 2Wᵀ·q

▲ x에 대한 gradient 계산 (출처: cs231n Lecture 4)

\(\frac{\partial f}{\partial W_{i,j}} = 2q_i x_j\)를 직관적으로 풀면 단순하다. 행렬 W의 특정 원소 \(W_{i,j}\)는 오직 i번째 행의 출력에만 영향을 준다. 그래서 그 원소의 gradient는 "위에서 흘러온 i번째 행의 gradient(\(2q_i\))" × "곱셈의 파트너였던 j번째 입력값(\(x_j\))"이라는 단순한 곱이다. 결국 \(\nabla_W f = 2q \cdot x^T\)가 나온다.

Modularized 구현: forward / backward API

코드로 구현할 때는 각 노드가 forward와 backward 메서드를 가지는 구조로 만든다. forward에서 중간값을 저장해두고, backward에서 꺼내 chain rule을 적용한다. PyTorch나 TensorFlow의 레이어 구조가 바로 이 방식이다.

Modularized forward/backward API

▲ 각 노드는 forward() + backward()를 구현 - PyTorch 구조의 원형 (출처: cs231n Lecture 4)

MultiplyGate 구현 예시

▲ forward에서 self.x, self.y 저장 → backward에서 서로 교환해서 곱함 (출처: cs231n Lecture 4)


8. Neural Networks

이제 backpropagation을 이해했으니 Neural Network로 넘어간다. \(f(x) = Wx\) 하나짜리 Linear Classifier를 여러 층으로 쌓은 게 신경망이다.

2-layer Neural Network 수식:

$$f = W_2 \cdot \max(0, W_1 x)$$

3-layer로 확장하면:

$$f = W_3 \max(0, W_2 \max(0, W_1 x))$$

왜 non-linearity가 필요한가?

Linear layer를 아무리 많이 쌓아도 수학적으로 하나의 Linear layer랑 동일하다. \(W_2(W_1 x) = (W_2 W_1)x\)이기 때문이다. 층을 쌓는 의미가 없어진다. 그래서 레이어 사이에 activation function이라는 non-linear 함수를 끼워 넣어야 복잡한 패턴을 학습할 수 있다.

생물 뉴런과의 유사성

인공 뉴런의 구조는 생물학적 뉴런에서 영감을 받았다. 여러 입력 신호를 가중치(시냅스)로 합산하고, activation function(세포체)을 통과시켜서 출력(축삭)을 내보내는 구조다. 다만 실제 뇌랑은 많은 차이가 있고, 지금은 수학적 모델로 이해하는 게 맞다.

생물 뉴런과 인공 뉴런 비교

▲ dendrite(입력) → cell body(합산+activation) → axon(출력) (출처: cs231n Lecture 4)

Activation Functions

함수 수식 특징
Sigmoid \(\frac{1}{1+e^{-x}}\) 출력 0~1. saturation → Vanishing Gradient
tanh \(\tanh(x)\) 출력 -1~1. sigmoid보다 낫지만 여전히 saturation
ReLU \(\max(0, x)\) 단순하고 빠름. 현재 가장 많이 씀. Dead ReLU 주의
Leaky ReLU \(\max(0.1x, x)\) Dead ReLU 완화. 음수 구간에도 작은 gradient 유지
Maxout \(\max(w_1^T x + b_1, w_2^T x + b_2)\) ReLU/Leaky ReLU의 일반화
ELU \(x\) if \(x \geq 0\), \(\alpha(e^x-1)\) if \(x < 0\) 음수 구간에서 smooth
Activation Functions 종류

▲ Sigmoid, tanh, ReLU, Leaky ReLU, Maxout, ELU 그래프 비교 (출처: cs231n Lecture 4)

Neural Network 구조

레이어를 쌓는 가장 기본적인 방식은 Fully-connected layer다. 앞 레이어의 모든 뉴런이 다음 레이어의 모든 뉴런에 연결되는 방식이다. 레이어 수를 셀 때는 입력 레이어를 제외하고 hidden + output만 센다. hidden이 1개이면 "2-layer Neural Net", 2개이면 "3-layer Neural Net"이다.

Neural Network Architectures

▲ 2-layer(hidden 1개) vs 3-layer(hidden 2개) fully-connected 구조 (출처: cs231n Lecture 4)


핵심 요약

개념 한 줄 요약
Computational Graph 복잡한 함수를 작은 연산 노드들의 연결로 표현. 노드 복잡도는 개발자가 결정
Forward pass 입력 → 출력 계산. 중간값 반드시 저장
Backward pass Chain Rule로 gradient를 뒤에서 앞으로 전파
Local gradient 해당 노드 자체의 입출력에 대한 미분
Upstream gradient 뒤쪽에서 흘러온 gradient. local gradient와 곱해서 앞으로 전달
Gradient의 의미 Loss에 대한 해당 변수의 민감도
Add gate gradient 그대로 분배 (distributor)
Mul gate 입력값을 서로 교환해서 곱해 전달 (switcher)
Max gate 더 큰 쪽으로만 gradient 전달 (router)
Sigmoid gradient \(\sigma(x)(1-\sigma(x))\). saturation 시 Vanishing Gradient
Gradient shape 원칙 변수의 gradient shape = 변수의 shape
Non-linearity 필요 이유 없으면 여러 층 쌓아도 Linear layer 하나랑 동일
ReLU \(\max(0,x)\). 빠르고 Vanishing Gradient 완화. Dead ReLU 주의
Fully-connected layer 앞 레이어 모든 뉴런이 다음 레이어 모든 뉴런에 연결

참고


다음: Lecture 5 - Convolutional Neural Networks