연쇄 법칙 ↔ 역전파 문서의 검증 코드다. if __name__ == "__main__": 아래가 자체 테스트다.
"""연쇄 법칙 ↔ 역전파 검증.
문서: 22.연쇄 법칙 ↔ 역전파 (비교 표, 대응 관계, 어디까지 같은가, 얻는 것, 전이 문제, 카드 C1~C3)
주장 1: 비교 표 — f = (x + y)y, (2,3): f = 15, df/dx = 3, df/dy = 8 (자동미분, 손 계산 y + u).
주장 2: 자동미분이 무작위 뉴런 손실 (σ(w·x + b) - t)² 200개에서 중앙 차분과 일치.
주장 3: 카드 C2 — (xy + x)², (1,2): 18, 6.
주장 4: 전이 문제 — C = AB, D = C + A, E = D², (2,3): 64, 32.
주장 5: 비용 — 1×n 기울기와 n×n 야코비 L개: 왼쪽부터 L n² 곱셈, 오른쪽부터 (L-1)n³ + n², 결과는 같다(유리수 무작위).
"""
import math
import random
from fractions import Fraction as F
class Value:
"""22_backprop-bridge_impl.py의 Value를 복사(파일끼리 import하지 않는다)."""
def __init__(self, data, parents=()):
self.data = float(data)
self.grad = 0.0
self._parents = parents
def __add__(self, o):
o = o if isinstance(o, Value) else Value(o)
return Value(self.data + o.data, ((self, 1.0), (o, 1.0)))
__radd__ = __add__
def __mul__(self, o):
o = o if isinstance(o, Value) else Value(o)
return Value(self.data * o.data, ((self, o.data), (o, self.data)))
__rmul__ = __mul__
def __sub__(self, o):
o = o if isinstance(o, Value) else Value(o)
return Value(self.data - o.data, ((self, 1.0), (o, -1.0)))
def __pow__(self, k):
return Value(self.data ** k, ((self, k * self.data ** (k - 1)),))
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) not in seen:
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
def mm(A, B):
return [[sum(A[i][k] * B[k][j] for k in range(len(B))) for j in range(len(B[0]))] for i in range(len(A))]
def main():
x, y = Value(2), Value(3)
u = x + y
f = u * y
f.backward()
assert (f.data, x.grad, y.grad) == (15, 3, 8) and 3 + 5 == 8
print("[OK] 주장 1·카드 C1: 비교 표")
rng = random.Random(22)
for _ in range(200):
vals = [rng.uniform(-2, 2) for _ in range(6)]
nodes = [Value(v) for v in vals]
w1, w2, b, x1, x2, t = nodes
L = ((w1 * x1 + w2 * x2 + b).sigmoid() - t) ** 2
L.backward()
def Ln(v):
s = 1 / (1 + math.exp(-(v[0] * v[3] + v[1] * v[4] + v[2])))
return (s - v[5]) ** 2
for i, nd in enumerate(nodes):
p, q = vals[:], vals[:]
p[i] += 1e-6
q[i] -= 1e-6
assert abs(nd.grad - (Ln(p) - Ln(q)) / 2e-6) < 1e-6
print("[OK] 주장 2: 자동미분 = 중앙 차분")
x, y = Value(1), Value(2)
f = (x * y + x) ** 2
f.backward()
assert (f.data, x.grad, y.grad) == (9, 18, 6)
print("[OK] 주장 3·카드 C2")
A, B = Value(2), Value(3)
C = A * B
D = C + A
E = D ** 2
E.backward()
assert (C.data, D.data, E.data, A.grad, B.grad) == (6, 8, 64, 64, 32)
print("[OK] 주장 4: 전이 문제")
for n, Lc in ((3, 4), (5, 3), (8, 5)):
g = [[F(rng.randint(-3, 3)) for _ in range(n)]]
Js = [[[F(rng.randint(-3, 3)) for _ in range(n)] for _ in range(n)] for _ in range(Lc)]
left, cost_l = g, 0
for J in Js:
left = mm(left, J)
cost_l += n * n
# 오른쪽부터: 야코비끼리의 행렬 곱을 먼저 모두 만든 뒤 마지막에 g를 곱한다
cost_r = 0
prod = Js[0]
for J in Js[1:]:
prod = mm(prod, J)
cost_r += n ** 3
res_r = mm(g, prod)
cost_r += n * n
assert res_r == left
assert cost_l == Lc * n * n and cost_r == (Lc - 1) * n ** 3 + n * n
print("[OK] 주장 5·카드 C3: 곱하는 순서와 비용")
print("ALL CHECKS PASSED")
if __name__ == "__main__":
main()