연쇄 법칙 ↔ 역전파 문서의 구현 코드다. if __name__ == "__main__": 아래가 자체 테스트다.
"""스칼라 역방향 자동미분(역전파)의 최소 구현.
문서: 22.연쇄 법칙 ↔ 역전파
Value는 값 하나와 기울기 하나를 들고, 자신을 만든 연산(부모와 국소 도함수)을 기억한다.
backward(): 출력에서 시작해 계산 그래프를 위상 정렬의 역순으로 훑으며
부모.grad += (국소 도함수) × 자신.grad
를 한다(연쇄 법칙). 한 값이 여러 곳에 쓰이면 기울기가 더해진다(여러 길의 합).
"""
import math
class Value:
def __init__(self, data, parents=()):
self.data = float(data)
self.grad = 0.0
self._parents = parents # [(부모 Value, 국소 도함수), ...]
def __add__(self, other):
other = other if isinstance(other, Value) else Value(other)
return Value(self.data + other.data, ((self, 1.0), (other, 1.0)))
__radd__ = __add__
def __mul__(self, other):
other = other if isinstance(other, Value) else Value(other)
return Value(self.data * other.data, ((self, other.data), (other, self.data)))
__rmul__ = __mul__
def __neg__(self):
return self * -1.0
def __sub__(self, other):
return self + (-other if isinstance(other, Value) else Value(-other))
def __pow__(self, k):
return Value(self.data ** k, ((self, k * self.data ** (k - 1)),))
def exp(self):
e = math.exp(self.data)
return Value(e, ((self, e),))
def sigmoid(self):
s = 1 / (1 + math.exp(-self.data))
return Value(s, ((self, s * (1 - s)),))
def backward(self):
order, seen = [], set()
def visit(v):
if id(v) in seen:
return
seen.add(id(v))
for p, _ in v._parents:
visit(p)
order.append(v)
visit(self)
self.grad = 1.0
for v in reversed(order):
for p, local in v._parents:
p.grad += local * v.grad
if __name__ == "__main__":
# 문서의 예: f = (x + y) * y, x = 2, y = 3 -> f = 15, df/dx = y = 3, df/dy = x + 2y = 8
x, y = Value(2), Value(3)
f = (x + y) * y
f.backward()
assert f.data == 15 and x.grad == 3 and y.grad == 8
# 수치 미분과 비교: 뉴런 하나 L = (σ(w1 x1 + w2 x2 + b) - t)²
import random
rng = random.Random(22)
for _ in range(200):
vals = [rng.uniform(-2, 2) for _ in range(6)]
w1, w2, b, x1, x2, t = (Value(v) for v in vals)
L = ((w1 * x1 + w2 * x2 + b).sigmoid() - t) ** 2
L.backward()
def Lnum(v):
s = 1 / (1 + math.exp(-(v[0] * v[3] + v[1] * v[4] + v[2])))
return (s - v[5]) ** 2
for i, node in enumerate((w1, w2, b, x1, x2, t)):
p, q = vals[:], vals[:]
p[i] += 1e-6
q[i] -= 1e-6
assert abs(node.grad - (Lnum(p) - Lnum(q)) / 2e-6) < 1e-6
print("ALL CHECKS PASSED")