본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 11. 28. · 22 Views
연쇄 법칙과 역전파 완벽 가이드
딥러닝의 핵심인 역전파 알고리즘을 이해하기 위한 필수 개념인 연쇄 법칙을 처음부터 차근차근 설명합니다. 수학이 어려워도 걱정 마세요. 비유와 실제 코드로 쉽게 이해할 수 있습니다.
목차
1. 연쇄 법칙
어느 날 김개발 씨가 딥러닝 강의를 듣다가 멈칫했습니다. 강사가 "역전파는 연쇄 법칙을 기반으로 합니다"라고 말하는데, 연쇄 법칙이 정확히 무엇인지 기억나지 않았기 때문입니다.
고등학교 수학 시간에 배운 것 같기도 한데, 이게 왜 딥러닝에서 그토록 중요한 걸까요?
**연쇄 법칙(Chain Rule)**은 합성 함수를 미분할 때 사용하는 규칙입니다. 쉽게 말해, 함수 안에 함수가 들어있을 때 미분하는 방법입니다.
마치 러시아 인형 마트료시카를 열어보는 것처럼, 바깥 함수를 미분하고 안쪽 함수도 미분해서 곱해주면 됩니다. 이 단순한 규칙이 신경망의 모든 가중치를 학습시키는 핵심 원리입니다.
다음 코드를 살펴봅시다.
import numpy as np
# 연쇄 법칙 예시: y = (3x + 2)^2
# 바깥 함수: u^2, 안쪽 함수: u = 3x + 2
def inner_function(x):
# 안쪽 함수: u = 3x + 2
return 3 * x + 2
def outer_function(u):
# 바깥 함수: y = u^2
return u ** 2
def chain_rule_derivative(x):
# 연쇄 법칙: dy/dx = dy/du * du/dx
u = inner_function(x)
dy_du = 2 * u # 바깥 함수 미분: 2u
du_dx = 3 # 안쪽 함수 미분: 3
return dy_du * du_dx # 두 미분을 곱함
x = 1
print(f"x={x}에서 기울기: {chain_rule_derivative(x)}") # 출력: 30
김개발 씨는 입사 6개월 차 머신러닝 엔지니어입니다. 요즘 딥러닝 모델을 직접 구현해보고 싶어서 밤마다 공부하고 있습니다.
그런데 역전파를 이해하려면 연쇄 법칙을 알아야 한다는데, 수학 공식만 보면 머리가 아파옵니다. 다행히 옆자리 박시니어 씨가 점심시간에 친절하게 설명해주었습니다.
"연쇄 법칙은 생각보다 간단해요. 택배 배송 과정을 떠올려보세요." 그렇다면 연쇄 법칙이란 정확히 무엇일까요?
쉽게 비유하자면, 연쇄 법칙은 마치 택배가 여러 물류센터를 거쳐 배송되는 것과 같습니다. 서울에서 부산까지 택배를 보낸다고 생각해봅시다.
서울 물류센터에서 대전으로, 대전에서 대구로, 대구에서 부산으로 이동합니다. 전체 배송 시간의 변화를 알고 싶다면, 각 구간의 변화를 모두 곱해야 합니다.
수학에서도 마찬가지입니다. **y = f(g(x))**처럼 함수가 겹쳐있을 때, y가 x에 대해 얼마나 변하는지 알려면 각 단계의 변화율을 곱해야 합니다.
이것이 바로 연쇄 법칙의 핵심입니다. 연쇄 법칙이 없던 시절에는 어땠을까요?
복잡한 합성 함수를 미분하려면 매번 함수를 전개해서 풀어야 했습니다. (3x + 2)의 2승만 해도 전개하면 9x의 2승 + 12x + 4가 됩니다.
미분하면 18x + 12가 나오죠. 간단한 함수라 다행이지만, 신경망처럼 수백 개의 함수가 겹쳐있다면 전개하는 것 자체가 불가능합니다.
바로 이런 문제를 해결하기 위해 연쇄 법칙을 사용합니다. 연쇄 법칙을 적용하면 복잡한 함수도 단계별로 쪼개서 미분할 수 있습니다.
위 코드에서 dy/dx = dy/du 곱하기 du/dx를 계산했습니다. u에서 y로 가는 변화율(2u)과 x에서 u로 가는 변화율(3)을 곱한 것입니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 inner_function은 안쪽 함수 u = 3x + 2를 정의합니다.
다음으로 outer_function은 바깥 함수 y = u의 2승을 정의합니다. chain_rule_derivative 함수에서는 연쇄 법칙을 적용합니다.
dy_du는 바깥 함수를 u에 대해 미분한 2u이고, du_dx는 안쪽 함수를 x에 대해 미분한 3입니다. 이 둘을 곱하면 최종 미분값이 됩니다.
실제 딥러닝에서는 어떻게 활용할까요? 신경망은 수십, 수백 개의 층으로 이루어져 있습니다.
각 층은 입력을 받아 출력을 내보내는 함수입니다. 전체 신경망은 이 함수들이 겹겹이 쌓인 거대한 합성 함수입니다.
학습할 때 각 가중치가 최종 오차에 얼마나 영향을 미치는지 계산해야 하는데, 연쇄 법칙 없이는 이 계산이 불가능합니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 곱셈 순서를 헷갈리는 것입니다. 연쇄 법칙에서는 바깥에서 안쪽으로 미분해 나갑니다.
또한 안쪽 함수의 미분값을 계산할 때, 원래의 안쪽 함수 값(u)이 필요하다는 점도 기억해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 설명을 들은 김개발 씨는 무릎을 탁 쳤습니다. "아, 그래서 forward pass에서 중간값을 저장해두는 거군요!" 연쇄 법칙을 제대로 이해하면 역전파가 왜 그렇게 동작하는지 명확하게 알 수 있습니다.
다음 장에서는 합성 함수의 미분을 더 자세히 살펴보겠습니다.
실전 팁
💡 - 연쇄 법칙은 바깥에서 안쪽으로, 미분값을 곱해 나가는 것입니다
- 중간 계산값(u)을 저장해두어야 미분을 계산할 수 있습니다
- 복잡해 보여도 한 단계씩 차근차근 적용하면 됩니다
2. 합성 함수의 미분
김개발 씨가 연쇄 법칙을 어느 정도 이해하고 나니, 새로운 의문이 생겼습니다. "그런데 함수가 세 개, 네 개 겹쳐있으면 어떻게 하죠?" 신경망은 층이 수십 개인데, 그때마다 연쇄 법칙을 적용하려면 어떻게 해야 할까요?
합성 함수의 미분은 연쇄 법칙을 여러 번 적용하는 것입니다. 함수가 세 개 겹쳐있다면 연쇄 법칙을 두 번 적용하면 됩니다.
마치 도미노가 연쇄적으로 쓰러지는 것처럼, 각 함수의 미분이 연쇄적으로 곱해집니다. 신경망의 깊이가 아무리 깊어도 이 원리는 동일하게 적용됩니다.
다음 코드를 살펴봅시다.
import numpy as np
# 세 개의 함수가 합성된 경우: y = sin((2x + 1)^3)
# h(x) = 2x + 1, g(u) = u^3, f(v) = sin(v)
def compute_forward(x):
# 순전파: 안쪽에서 바깥으로 계산
u = 2 * x + 1 # 첫 번째 함수
v = u ** 3 # 두 번째 함수
y = np.sin(v) # 세 번째 함수
return u, v, y
def compute_gradient(x):
# 연쇄 법칙: dy/dx = dy/dv * dv/du * du/dx
u, v, y = compute_forward(x)
dy_dv = np.cos(v) # sin의 미분은 cos
dv_du = 3 * (u ** 2) # u^3의 미분은 3u^2
du_dx = 2 # 2x+1의 미분은 2
return dy_dv * dv_du * du_dx # 모두 곱함
x = 0.5
print(f"x={x}에서 기울기: {compute_gradient(x):.4f}")
점심시간이 끝나고 김개발 씨는 다시 공부를 시작했습니다. 연쇄 법칙은 이해했는데, 실제 신경망은 함수가 두 개만 겹쳐있는 게 아닙니다.
수십 개의 층을 어떻게 처리해야 할까요? 박시니어 씨가 화이트보드 앞으로 김개발 씨를 불렀습니다.
"도미노 게임 해본 적 있죠? 합성 함수 미분도 똑같아요." 합성 함수란 함수 속에 함수가 들어있는 구조를 말합니다.
**y = sin((2x + 1)의 3승)**을 예로 들어보겠습니다. 이 함수는 세 단계로 이루어져 있습니다.
먼저 2x + 1을 계산하고, 그 결과를 세제곱하고, 마지막으로 sin을 취합니다. 이것을 미분하려면 어떻게 해야 할까요?
마치 도미노처럼 생각하면 됩니다. 첫 번째 도미노가 쓰러지면 두 번째가 쓰러지고, 두 번째가 쓰러지면 세 번째가 쓰러집니다.
각 도미노가 다음 도미노에 전달하는 힘을 모두 곱하면 맨 처음 도미노가 맨 마지막 도미노에 미치는 영향을 알 수 있습니다. 수학에서는 이것을 dy/dx = dy/dv 곱하기 dv/du 곱하기 du/dx로 표현합니다.
위 코드를 살펴보면, compute_forward 함수에서 순전파를 수행합니다. x로부터 u, v, y를 차례로 계산합니다.
여기서 중요한 점은 중간값 u와 v를 저장한다는 것입니다. 나중에 미분을 계산할 때 이 값들이 필요하기 때문입니다.
compute_gradient 함수에서는 연쇄 법칙을 적용합니다. sin(v)를 v에 대해 미분하면 cos(v)가 됩니다.
u의 3승을 u에 대해 미분하면 3u의 2승이 됩니다. 2x + 1을 x에 대해 미분하면 2가 됩니다.
이 세 값을 모두 곱하면 최종 기울기가 나옵니다. 실제 신경망에서는 어떻게 적용될까요?
10층짜리 신경망이 있다고 가정해봅시다. 입력이 첫 번째 층을 통과하고, 그 출력이 두 번째 층의 입력이 되고, 이런 식으로 열 번째 층까지 연결됩니다.
전체 네트워크는 10개의 함수가 합성된 거대한 합성 함수입니다. 첫 번째 층의 가중치가 최종 출력에 얼마나 영향을 미치는지 계산하려면, 10개의 미분값을 모두 곱해야 합니다.
연쇄 법칙을 9번 적용하는 것이죠. 여기서 중요한 통찰이 있습니다.
순전파 과정에서 계산한 중간값들을 저장해두면, 역전파에서 재사용할 수 있습니다. 이것이 딥러닝 프레임워크가 메모리를 많이 사용하는 이유 중 하나입니다.
중간 계산 결과를 모두 저장해야 역전파를 효율적으로 수행할 수 있기 때문입니다. 주의할 점도 있습니다.
함수가 많이 겹칠수록 기울기가 매우 작아지거나(기울기 소실) 매우 커질 수(기울기 폭발) 있습니다. 0.9를 10번 곱하면 0.35가 되고, 100번 곱하면 거의 0에 가까워집니다.
이것이 바로 딥러닝에서 유명한 기울기 소실 문제입니다. 김개발 씨가 고개를 끄덕였습니다.
"그래서 ReLU나 ResNet 같은 기법이 나온 거군요. 기울기가 잘 전달되도록요." 합성 함수의 미분을 이해하면 신경망의 학습 원리가 보이기 시작합니다.
다음 장에서는 이것을 시각적으로 표현하는 계산 그래프를 알아보겠습니다.
실전 팁
💡 - 합성 함수 미분은 연쇄 법칙을 반복 적용하는 것입니다
- 순전파에서 중간값을 저장해야 역전파에서 사용할 수 있습니다
- 층이 깊어지면 기울기 소실/폭발 문제를 고려해야 합니다
3. 계산 그래프 이해
김개발 씨는 연쇄 법칙을 수식으로는 이해했지만, 뭔가 머릿속에서 명확하게 그려지지 않았습니다. "이걸 좀 더 직관적으로 볼 수 있는 방법이 없을까요?" 박시니어 씨가 빈 종이를 꺼내며 말했습니다.
"계산 그래프를 그려볼까요?"
**계산 그래프(Computational Graph)**는 수학 연산을 노드와 엣지로 시각화한 것입니다. 각 노드는 하나의 연산(덧셈, 곱셈 등)을 나타내고, 엣지는 데이터의 흐름을 나타냅니다.
마치 공장의 조립 라인처럼, 데이터가 여러 연산을 거쳐 최종 결과물이 됩니다. 이 그래프를 따라 역방향으로 가면 기울기를 계산할 수 있습니다.
다음 코드를 살펴봅시다.
# 계산 그래프 예시: z = (x + y) * y
# 노드1: a = x + y (덧셈)
# 노드2: z = a * y (곱셈)
class AddNode:
def forward(self, x, y):
self.x, self.y = x, y # 입력 저장
return x + y
def backward(self, dout):
# 덧셈의 역전파: 그대로 전달
return dout, dout # dx = dout, dy = dout
class MulNode:
def forward(self, x, y):
self.x, self.y = x, y # 입력 저장
return x * y
def backward(self, dout):
# 곱셈의 역전파: 서로 바꿔서 곱함
return dout * self.y, dout * self.x
# 계산 그래프 실행
x, y = 2, 3
add_node = AddNode()
mul_node = MulNode()
# 순전파
a = add_node.forward(x, y) # a = 2 + 3 = 5
z = mul_node.forward(a, y) # z = 5 * 3 = 15
# 역전파 (dz/dz = 1에서 시작)
da, dy1 = mul_node.backward(1) # da = 3, dy1 = 5
dx, dy2 = add_node.backward(da) # dx = 3, dy2 = 3
print(f"dz/dx = {dx}, dz/dy = {dy1 + dy2}") # y는 두 경로로 영향
박시니어 씨가 종이에 동그라미 두 개와 화살표를 그리기 시작했습니다. "계산 그래프는 연산을 그림으로 그린 거예요.
보면 바로 이해될 겁니다." 계산 그래프란 무엇일까요? 마치 공장의 조립 라인을 위에서 내려다보는 것과 같습니다.
원재료(입력)가 들어와서 여러 작업대(연산 노드)를 거치며 가공되고, 최종적으로 완제품(출력)이 나옵니다. 각 작업대에서는 단 하나의 작업만 수행합니다.
덧셈이면 덧셈만, 곱셈이면 곱셈만 합니다. 위 코드에서 z = (x + y) 곱하기 y를 계산 그래프로 표현했습니다.
첫 번째 노드(AddNode)에서는 x와 y를 더해 a를 만듭니다. 두 번째 노드(MulNode)에서는 a와 y를 곱해 z를 만듭니다.
여기서 중요한 점이 있습니다. 각 노드는 **순전파(forward)**와 역전파(backward) 두 가지 기능을 가집니다.
순전파에서는 입력을 받아 출력을 계산합니다. 동시에 입력값을 저장해둡니다.
역전파에서는 위쪽(출력 방향)에서 전달된 기울기를 받아, 아래쪽(입력 방향)으로 전달할 기울기를 계산합니다. 덧셈 노드의 역전파는 어떻게 동작할까요?
z = x + y에서 dz/dx는 1이고, dz/dy도 1입니다. 따라서 위에서 내려온 기울기(dout)를 그대로 양쪽으로 전달합니다.
덧셈의 역전파는 기울기를 복사해서 전달하는 것입니다. 곱셈 노드의 역전파는 다릅니다.
z = x 곱하기 y에서 dz/dx는 y이고, dz/dy는 x입니다. 따라서 위에서 내려온 기울기에 상대방의 값을 곱해서 전달합니다.
x 방향으로는 y를 곱하고, y 방향으로는 x를 곱합니다. 곱셈의 역전파는 값을 서로 바꿔서 곱하는 것입니다.
코드에서 재미있는 부분이 있습니다. y가 두 군데에서 사용되었습니다.
덧셈 노드에도 들어가고, 곱셈 노드에도 직접 들어갑니다. 이런 경우 y에 대한 기울기는 두 경로에서 오는 기울기를 더해야 합니다.
mul_node에서 온 dy1과 add_node에서 온 dy2를 합해서 최종 dz/dy를 구합니다. 실제 딥러닝 프레임워크에서는 어떻게 활용할까요?
TensorFlow나 PyTorch 같은 프레임워크는 여러분이 연산을 수행할 때 자동으로 계산 그래프를 구축합니다. 그리고 loss.backward()를 호출하면 이 그래프를 역방향으로 따라가며 모든 기울기를 자동으로 계산합니다.
이것이 바로 자동 미분의 원리입니다. 김개발 씨의 눈이 반짝였습니다.
"아, 그래서 PyTorch에서 requires_grad=True를 설정하면 그래프가 만들어지는 거군요!" 계산 그래프를 이해하면 딥러닝 프레임워크의 내부 동작이 명확해집니다. 다음 장에서는 순전파와 역전파가 실제로 어떻게 진행되는지 더 자세히 알아보겠습니다.
실전 팁
💡 - 덧셈의 역전파는 기울기를 그대로 복사해서 전달합니다
- 곱셈의 역전파는 입력값을 서로 바꿔서 기울기에 곱합니다
- 한 변수가 여러 곳에서 사용되면 기울기를 모두 더해야 합니다
4. 순전파와 역전파
이제 김개발 씨는 개별 노드의 순전파와 역전파를 이해했습니다. 하지만 전체 신경망에서 이것이 어떻게 동작하는지는 아직 감이 잡히지 않았습니다.
"처음부터 끝까지 한 번 따라가볼 수 있을까요?" 박시니어 씨가 간단한 신경망 예제를 준비했습니다.
**순전파(Forward Propagation)**는 입력에서 출력 방향으로 계산을 진행하는 과정입니다. **역전파(Backward Propagation)**는 출력에서 입력 방향으로 기울기를 전파하는 과정입니다.
마치 물이 위에서 아래로 흐르는 것이 순전파라면, 역전파는 그 물의 흐름을 역추적하는 것과 같습니다. 이 두 과정을 반복하면서 신경망은 학습합니다.
다음 코드를 살펴봅시다.
import numpy as np
# 간단한 2층 신경망: 입력 -> 은닉층 -> 출력
class SimpleNetwork:
def __init__(self):
self.w1 = np.array([[0.1, 0.2], [0.3, 0.4]]) # 2x2 가중치
self.w2 = np.array([[0.5], [0.6]]) # 2x1 가중치
def sigmoid(self, x):
return 1 / (1 + np.exp(-x))
def forward(self, x):
# 순전파: 입력 -> 출력
self.x = x
self.z1 = np.dot(x, self.w1) # 선형 변환
self.a1 = self.sigmoid(self.z1) # 활성화 함수
self.z2 = np.dot(self.a1, self.w2) # 출력층
return self.z2
def backward(self, dout):
# 역전파: 출력 -> 입력
dw2 = np.dot(self.a1.T, dout) # w2의 기울기
da1 = np.dot(dout, self.w2.T) # 은닉층으로 전파
dz1 = da1 * self.a1 * (1 - self.a1) # sigmoid의 미분
dw1 = np.dot(self.x.T, dz1) # w1의 기울기
return dw1, dw2
net = SimpleNetwork()
x = np.array([[1.0, 2.0]]) # 입력
output = net.forward(x)
dw1, dw2 = net.backward(np.array([[1.0]]))
print(f"출력: {output[0][0]:.4f}")
print(f"w1 기울기:\n{dw1}")
박시니어 씨가 화이트보드에 간단한 신경망을 그렸습니다. 입력층 2개, 은닉층 2개, 출력층 1개로 이루어진 작은 네트워크입니다.
"이 네트워크로 순전파와 역전파를 처음부터 끝까지 따라가봅시다." 먼저 순전파를 살펴보겠습니다. 순전파는 마치 물이 산 정상에서 계곡으로 흘러내리는 것과 같습니다.
입력 데이터가 첫 번째 층에 들어가면, 가중치와 곱해지고 활성화 함수를 통과합니다. 그 결과가 다음 층의 입력이 되고, 다시 가중치와 곱해지고...
이 과정이 마지막 출력층까지 반복됩니다. 코드에서 forward 메서드를 보면, x와 w1을 행렬 곱합니다.
그 결과 z1에 sigmoid 함수를 적용해 a1을 얻습니다. 다시 a1과 w2를 행렬 곱해서 최종 출력 z2를 얻습니다.
여기서 핵심적인 부분이 있습니다. 순전파를 하면서 중간값들(x, z1, a1)을 모두 저장해둡니다.
이 값들이 역전파에서 필요하기 때문입니다. 이제 역전파를 살펴보겠습니다.
역전파는 마치 탐정이 범인을 추적하는 것과 같습니다. 최종 결과(범죄 현장)에서 시작해서, 각 단계를 역추적하며 "누가 얼마나 기여했는지"를 계산합니다.
출력의 오차가 각 가중치에 얼마나 영향을 받았는지 거슬러 올라가며 계산하는 것입니다. backward 메서드에서는 출력층부터 시작합니다.
dout은 위에서 전달된 기울기입니다. 먼저 w2의 기울기를 계산합니다.
dw2 = a1의 전치 행렬 곱하기 dout입니다. 이것은 "a1이 w2를 통해 출력에 얼마나 기여했는가"를 나타냅니다.
다음으로 은닉층으로 기울기를 전파합니다. da1 = dout 곱하기 w2의 전치 행렬입니다.
이것은 w2를 거쳐 역전파된 기울기입니다. sigmoid 함수의 역전파도 중요합니다.
sigmoid의 미분은 **sigmoid(x) 곱하기 (1 - sigmoid(x))**입니다. 코드에서 **a1 곱하기 (1 - a1)**이 바로 이것입니다.
da1에 이 값을 곱해서 dz1을 구합니다. 마지막으로 w1의 기울기를 계산합니다.
dw1 = x의 전치 행렬 곱하기 dz1입니다. 실제 학습에서는 어떻게 사용할까요?
계산된 기울기(dw1, dw2)를 사용해서 가중치를 업데이트합니다. w1 = w1 - 학습률 곱하기 dw1, w2 = w2 - 학습률 곱하기 dw2 이렇게 말입니다.
이 과정을 수천, 수만 번 반복하면 신경망이 학습됩니다. 주의할 점이 있습니다.
역전파에서 기울기가 0에 가까워지면 학습이 거의 일어나지 않습니다. sigmoid 함수의 경우 입력이 매우 크거나 작으면 미분값이 거의 0이 됩니다.
이것이 기울기 포화(gradient saturation) 문제입니다. 이 때문에 요즘은 ReLU 같은 활성화 함수를 더 많이 사용합니다.
김개발 씨가 감탄했습니다. "이제 역전파가 마법이 아니라 수학적 계산이라는 게 확실히 느껴집니다!" 순전파와 역전파를 이해하면 신경망 학습의 전체 그림이 보입니다.
다음 장에서는 이 과정을 자동화하는 자동 미분 개념을 알아보겠습니다.
실전 팁
💡 - 순전파에서 중간값을 저장해야 역전파에서 사용할 수 있습니다
- 역전파는 연쇄 법칙을 출력층부터 입력층까지 적용하는 것입니다
- 활성화 함수의 미분 특성이 학습 효율에 큰 영향을 미칩니다
5. 자동 미분 개념
김개발 씨는 순전파와 역전파를 직접 구현해보니 꽤 복잡하다는 것을 느꼈습니다. "이걸 매번 직접 작성해야 하나요?" 박시니어 씨가 웃으며 대답했습니다.
"아니요, 딥러닝 프레임워크가 알아서 해줍니다. 그게 바로 자동 미분이에요."
**자동 미분(Automatic Differentiation)**은 컴퓨터가 수학 함수의 미분을 자동으로 계산하는 기술입니다. 프로그래머가 순전파 코드만 작성하면, 프레임워크가 계산 그래프를 추적해서 역전파를 자동으로 수행합니다.
마치 네비게이션이 목적지까지의 경로를 자동으로 찾아주는 것처럼, 자동 미분은 기울기를 자동으로 계산해줍니다.
다음 코드를 살펴봅시다.
# 자동 미분의 핵심 개념을 간단히 구현
class Variable:
def __init__(self, value, requires_grad=True):
self.value = value
self.grad = 0.0
self.requires_grad = requires_grad
self._backward = lambda: None # 역전파 함수
self._prev = set() # 부모 노드들
def __mul__(self, other):
# 곱셈 연산 정의
out = Variable(self.value * other.value)
out._prev = {self, other}
def _backward():
# 곱셈의 역전파: 서로의 값을 곱함
self.grad += other.value * out.grad
other.grad += self.value * out.grad
out._backward = _backward
return out
def __add__(self, other):
out = Variable(self.value + other.value)
out._prev = {self, other}
def _backward():
# 덧셈의 역전파: 그대로 전달
self.grad += out.grad
other.grad += out.grad
out._backward = _backward
return out
def backward(self):
# 위상 정렬로 역전파 순서 결정
topo = []
visited = set()
def build_topo(v):
if v not in visited:
visited.add(v)
for parent in v._prev:
build_topo(parent)
topo.append(v)
build_topo(self)
self.grad = 1.0
for node in reversed(topo):
node._backward()
# 사용 예시
x = Variable(2.0)
y = Variable(3.0)
z = x * y + x # z = xy + x = 2*3 + 2 = 8
z.backward()
print(f"z = {z.value}, dz/dx = {x.grad}, dz/dy = {y.grad}")
김개발 씨는 직접 역전파를 구현하면서 실수하기 쉽다는 것을 깨달았습니다. 행렬의 전치를 잘못 적용하거나, 기울기를 더해야 할 곳에서 대입해버리거나...
박시니어 씨가 말했습니다. "그래서 자동 미분이 있는 거예요." 자동 미분이란 무엇일까요?
마치 자동차의 네비게이션과 같습니다. 예전에는 지도를 보며 직접 경로를 찾아야 했습니다.
이 사거리에서 우회전, 저 신호등에서 좌회전... 하나라도 놓치면 길을 잃었습니다.
네비게이션은 이 과정을 자동화합니다. 목적지만 입력하면 알아서 경로를 계산해줍니다.
자동 미분도 마찬가지입니다. 프로그래머는 순전파 연산만 정의합니다.
z = x 곱하기 y + x 이렇게요. 그러면 프레임워크가 자동으로 계산 그래프를 구축하고, backward()를 호출하면 모든 변수의 기울기를 자동으로 계산해줍니다.
위 코드는 자동 미분의 핵심 원리를 보여줍니다. Variable 클래스는 값(value)과 기울기(grad)를 함께 저장합니다.
그리고 중요한 것은 _backward 함수와 _prev 집합입니다. _backward는 역전파 때 실행할 함수를 저장하고, _prev는 이 노드를 만드는 데 사용된 부모 노드들을 저장합니다.
곱셈 연산(mul)을 보면, 결과 Variable을 만들면서 동시에 역전파 함수를 정의합니다. 이 함수는 나중에 backward()가 호출될 때 실행됩니다.
곱셈의 역전파는 서로의 값을 곱해서 기울기에 더하는 것입니다. 덧셈 연산(add)에서는 기울기를 그대로 전달합니다.
양쪽 부모 노드에 동일한 기울기가 더해집니다. backward 메서드가 핵심입니다.
**위상 정렬(Topological Sort)**을 사용해서 역전파 순서를 결정합니다. 출력 노드에서 시작해서, 자식 노드를 모두 처리한 후에 부모 노드를 처리합니다.
이렇게 하면 의존성 순서에 맞게 역전파가 진행됩니다. 자동 미분에는 두 가지 모드가 있습니다.
**순방향 모드(Forward Mode)**는 입력에서 출력 방향으로 미분을 계산합니다. **역방향 모드(Reverse Mode)**는 출력에서 입력 방향으로 미분을 계산합니다.
딥러닝에서는 출력(손실)이 하나이고 입력(가중치)이 많으므로, 역방향 모드가 훨씬 효율적입니다. PyTorch와 TensorFlow 모두 역방향 모드를 사용합니다.
실제로 어떤 장점이 있을까요? 첫째, 실수가 줄어듭니다.
수학적으로 정확한 기울기가 자동으로 계산됩니다. 둘째, 개발 속도가 빨라집니다.
순전파만 작성하면 되니까요. 셋째, 복잡한 모델도 쉽게 만들 수 있습니다.
조건문, 반복문이 포함된 동적 계산 그래프도 처리할 수 있습니다. 김개발 씨가 감탄했습니다.
"이제 PyTorch가 어떻게 기울기를 계산하는지 알 것 같아요!" 다음 장에서는 실제 PyTorch의 autograd를 사용해보겠습니다.
실전 팁
💡 - 자동 미분은 계산 그래프를 추적하여 역전파를 자동화합니다
- 딥러닝에서는 역방향 모드 자동 미분이 효율적입니다
- 순전파 코드만 작성하면 기울기는 프레임워크가 계산합니다
6. PyTorch autograd 미리보기
마침내 김개발 씨는 실제 PyTorch 코드를 작성할 준비가 되었습니다. 지금까지 배운 연쇄 법칙, 계산 그래프, 자동 미분이 PyTorch에서 어떻게 구현되어 있는지 직접 확인해볼 시간입니다.
박시니어 씨가 PyCharm을 열었습니다. "자, 이제 진짜 코드를 작성해볼까요?"
PyTorch autograd는 PyTorch의 자동 미분 엔진입니다. 텐서에 requires_grad=True를 설정하면 모든 연산이 추적되고, .backward()를 호출하면 자동으로 기울기가 계산됩니다.
마치 녹화 버튼을 누르면 모든 행동이 기록되듯이, requires_grad가 설정된 텐서의 모든 연산은 계산 그래프에 기록됩니다.
다음 코드를 살펴봅시다.
import torch
# requires_grad=True로 기울기 추적 활성화
x = torch.tensor([2.0], requires_grad=True)
y = torch.tensor([3.0], requires_grad=True)
# 순전파: 자동으로 계산 그래프 생성
z = x * y + x ** 2 # z = xy + x^2
# 역전파: 모든 기울기 자동 계산
z.backward()
# 결과 확인
print(f"z = {z.item():.1f}") # z = 2*3 + 4 = 10
print(f"dz/dx = {x.grad.item():.1f}") # dz/dx = y + 2x = 3 + 4 = 7
print(f"dz/dy = {y.grad.item():.1f}") # dz/dy = x = 2
# 기울기 초기화 (반복 학습 시 필요)
x.grad.zero_()
y.grad.zero_()
# 실제 학습 루프 예시
for epoch in range(3):
z = x * y + x ** 2
z.backward()
# 경사 하강법: 기울기 반대 방향으로 이동
with torch.no_grad():
x -= 0.1 * x.grad
y -= 0.1 * y.grad
x.grad.zero_()
y.grad.zero_()
print(f"Epoch {epoch+1}: x={x.item():.3f}, y={y.item():.3f}")
김개발 씨가 드디어 PyTorch 코드를 직접 실행해봅니다. 지금까지 배운 개념들이 실제로 어떻게 동작하는지 확인할 시간입니다.
먼저 requires_grad=True가 핵심입니다. torch.tensor를 만들 때 requires_grad=True를 설정하면, 이 텐서와 관련된 모든 연산이 계산 그래프에 기록됩니다.
마치 CCTV가 모든 움직임을 녹화하는 것처럼, PyTorch가 모든 연산을 추적합니다. 코드에서 z = x 곱하기 y + x의 2승을 계산하면, PyTorch는 내부적으로 계산 그래프를 구축합니다.
곱셈 노드, 거듭제곱 노드, 덧셈 노드가 연결되어 z까지 이어집니다. backward()를 호출하면 마법이 일어납니다.
z.backward()를 호출하는 순간, PyTorch는 계산 그래프를 역방향으로 탐색하며 연쇄 법칙을 적용합니다. x.grad에는 dz/dx가, y.grad에는 dz/dy가 자동으로 저장됩니다.
수학적으로 검증해볼까요? z = xy + x의 2승이므로, dz/dx = y + 2x = 3 + 4 = 7이고, dz/dy = x = 2입니다.
코드 실행 결과와 정확히 일치합니다. 여기서 중요한 주의사항이 있습니다.
backward()를 여러 번 호출하면 기울기가 누적됩니다. 이전 기울기에 새 기울기가 더해지는 것입니다.
따라서 반복 학습을 할 때는 매번 **grad.zero_()**로 기울기를 초기화해야 합니다. 코드의 학습 루프를 살펴보겠습니다.
for 루프에서 매 epoch마다 순전파, 역전파, 가중치 업데이트를 반복합니다. 순전파에서 z를 계산하고, backward()로 기울기를 구하고, 경사 하강법으로 x와 y를 업데이트합니다.
torch.no_grad() 컨텍스트가 중요합니다. 가중치를 업데이트할 때는 이 연산이 계산 그래프에 포함되면 안 됩니다.
순수하게 값만 변경하는 것이니까요. no_grad()는 일시적으로 기울기 추적을 비활성화합니다.
실제 딥러닝 학습에서는 어떻게 다를까요? 실제로는 손실 함수(loss function)를 정의하고, 그 손실에 대해 backward()를 호출합니다.
또한 optimizer를 사용해서 가중치 업데이트를 자동화합니다. 기본 원리는 위 코드와 완전히 동일하지만, 더 편리한 API가 제공될 뿐입니다.
몇 가지 실용적인 팁을 알려드립니다. **detach()**는 텐서를 계산 그래프에서 분리합니다.
특정 부분의 기울기를 막고 싶을 때 사용합니다. **requires_grad_()**는 기존 텐서의 requires_grad 속성을 변경합니다.
retain_graph=True를 backward()에 전달하면 계산 그래프를 유지해서 여러 번 역전파할 수 있습니다. 김개발 씨가 코드를 실행해보며 감탄했습니다.
"정말 간단하네요! 순전파만 작성하면 기울기는 PyTorch가 알아서 계산해주는 거군요." 박시니어 씨가 고개를 끄덕였습니다.
"맞아요. 하지만 내부 원리를 이해하고 있으면, 문제가 생겼을 때 디버깅하기가 훨씬 쉬워요.
오늘 배운 연쇄 법칙과 계산 그래프를 기억해두세요." 이것으로 연쇄 법칙과 역전파의 여정이 끝났습니다. 수학 공식이 어렵게 느껴졌던 분들도 이제 딥러닝의 핵심 원리를 이해하셨을 겁니다.
직접 코드를 작성하고 실험해보면서 더 깊이 있는 이해를 쌓아가시길 바랍니다.
실전 팁
💡 - requires_grad=True가 설정된 텐서만 기울기가 계산됩니다
- 반복 학습 시 grad.zero_()로 기울기를 초기화해야 합니다
- torch.no_grad() 안에서는 계산 그래프가 생성되지 않습니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
Helm 마이크로서비스 패키징 완벽 가이드
Kubernetes 환경에서 마이크로서비스를 효율적으로 패키징하고 배포하는 Helm의 핵심 기능을 실무 중심으로 학습합니다. Chart 생성부터 릴리스 관리까지 체계적으로 다룹니다.
보안 아키텍처 구성 완벽 가이드
프로젝트의 보안을 처음부터 설계하는 방법을 배웁니다. AWS 환경에서 VPC부터 WAF, 암호화, 접근 제어까지 실무에서 바로 적용할 수 있는 보안 아키텍처를 단계별로 구성해봅니다.
AWS Organizations 완벽 가이드
여러 AWS 계정을 체계적으로 관리하고 통합 결제와 보안 정책을 적용하는 방법을 실무 스토리로 쉽게 배워봅니다. 초보 개발자도 바로 이해할 수 있는 친절한 설명과 실전 예제를 제공합니다.
AWS KMS 암호화 완벽 가이드
AWS KMS(Key Management Service)를 활용한 클라우드 데이터 암호화 방법을 초급 개발자를 위해 쉽게 설명합니다. CMK 생성부터 S3, EBS 암호화, 봉투 암호화까지 실무에 필요한 모든 내용을 담았습니다.
AWS Secrets Manager 완벽 가이드
AWS에서 데이터베이스 비밀번호, API 키 등 민감한 정보를 안전하게 관리하는 Secrets Manager의 핵심 개념과 실무 활용법을 배워봅니다. 초급 개발자도 쉽게 따라할 수 있도록 실전 예제와 함께 설명합니다.