다변수 연쇄 법칙과 야코비 행렬 문서의 검증 코드다. if __name__ == "__main__": 아래가 자체 테스트다.
"""다변수 연쇄 법칙과 야코비 행렬 검증.
문서: 21.다변수 연쇄 법칙과 야코비 행렬 (예시, 정의, 정리, 증명, 가정, 예제, 활용, 오해, 카드 C1~C4)
주장 1: 예시 — z = uv, u = x², v = sin x: 두 길의 합 2x sin x + x² cos x = (x² sin x)', 한 길만 쓰면 틀림.
주장 2: 무작위 합성(R² -> R³ -> R², R³ -> R² -> R¹ 등) — 수치 J_{g∘f} = J_g(f(a)) J_f(a).
주장 3: 선형 근사 — |f(a+h) - f(a) - J h| / |h|가 |h| -> 0에서 0으로(비율이 h에 비례해 줄어듦).
주장 4: 가정 — g = x²y/(x²+y²), f(t) = (t,t): 실제 (g∘f)'(0) = 1/2, 편미분 공식 0.
주장 5: 예제·카드 C4 — 극좌표 야코비와 행렬식 r, 원 운동 속도.
주장 6: 활용 — 층 y = σ(Wx)의 야코비 = diag(σ'(Wx)) W (수치).
주장 7: 카드 C2 — z = x²y, x = cos t, y = sin t: dz/dt = -2cos t sin² t + cos³ t.
"""
import math
import random
def jac(F, a, h=1e-6):
m = len(F(*a))
n = len(a)
J = [[0.0] * n for _ in range(m)]
for j in range(n):
p, q = list(a), list(a)
p[j] += h
q[j] -= h
Fp, Fq = F(*p), F(*q)
for i in range(m):
J[i][j] = (Fp[i] - Fq[i]) / (2 * h)
return J
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():
z = lambda x: (x * x) * math.sin(x)
for x in (0.3, 1.2, -2.0):
d = (z(x + 1e-6) - z(x - 1e-6)) / 2e-6
both = 2 * x * math.sin(x) + x * x * math.cos(x)
one = 2 * x * math.sin(x)
assert abs(d - both) < 1e-7 and abs(d - one) > 1e-3
print("[OK] 주장 1·오해: 두 길의 합")
rng = random.Random(21)
f = lambda x, y: (x * y, math.sin(x) + y, math.exp(0.3 * x - 0.2 * y))
g = lambda u, v, w: (u * v + w, math.cos(u) * w)
fs = lambda x, y, z: (x * y * z, x + z * z)
gs = lambda u, v: (math.sin(u) * v,)
for _ in range(100):
a = (rng.uniform(-1, 1), rng.uniform(-1, 1))
lhs = jac(lambda x, y: g(*f(x, y)), a)
rhs = mm(jac(g, f(*a)), jac(f, a))
assert all(abs(lhs[i][j] - rhs[i][j]) < 1e-6 for i in range(2) for j in range(2))
b = (rng.uniform(-1, 1), rng.uniform(-1, 1), rng.uniform(-1, 1))
lhs = jac(lambda x, y, zz: gs(*fs(x, y, zz)), b)
rhs = mm(jac(gs, fs(*b)), jac(fs, b))
assert all(abs(lhs[0][j] - rhs[0][j]) < 1e-6 for j in range(3))
print("[OK] 주장 2·카드 C1·C3: J_{g∘f} = J_g J_f")
a = (0.4, -0.3)
J = jac(f, a)
ratios = []
for s in (1e-1, 1e-2, 1e-3):
hvec = (0.6 * s, 0.8 * s)
Fa, Fh = f(*a), f(a[0] + hvec[0], a[1] + hvec[1])
lin = [Fa[i] + sum(J[i][j] * hvec[j] for j in range(2)) for i in range(3)]
err = math.sqrt(sum((Fh[i] - lin[i]) ** 2 for i in range(3)))
ratios.append(err / s)
assert ratios[0] > ratios[1] > ratios[2] and ratios[2] < 1e-2
print("[OK] 주장 3: 선형 근사 오차가 |h|보다 빨리 줄어듦")
G = lambda x, y: x * x * y / (x * x + y * y) if (x, y) != (0, 0) else 0.0
for t in (1e-2, 1e-6):
assert abs(G(t, t) / t - 0.5) < 1e-12
gx = (G(1e-8, 0) - G(-1e-8, 0)) / 2e-8
gy = (G(0, 1e-8) - G(0, -1e-8)) / 2e-8
assert gx * 1 + gy * 1 == 0
print("[OK] 주장 4: 미분 가능하지 않으면 공식이 틀림(1/2 대 0)")
P = lambda r, th: (r * math.cos(th), r * math.sin(th))
for _ in range(100):
r, th = rng.uniform(0.1, 3), rng.uniform(0, 2 * math.pi)
Jp = jac(P, (r, th))
exact = [[math.cos(th), -r * math.sin(th)], [math.sin(th), r * math.cos(th)]]
assert all(abs(Jp[i][j] - exact[i][j]) < 1e-6 for i in range(2) for j in range(2))
assert abs(Jp[0][0] * Jp[1][1] - Jp[0][1] * Jp[1][0] - r) < 1e-6
for t in (0.3, 2.0):
v = [row[1] for row in jac(P, (2, t))]
assert abs(v[0] + 2 * math.sin(t)) < 1e-6 and abs(v[1] - 2 * math.cos(t)) < 1e-6
print("[OK] 주장 5·예제·카드 C4: 극좌표")
sig = lambda s: 1 / (1 + math.exp(-s))
W = [[rng.uniform(-1, 1) for _ in range(3)] for _ in range(2)]
layer = lambda x, y, zz: tuple(sig(sum(W[i][j] * v for j, v in enumerate((x, y, zz)))) for i in range(2))
x0 = (0.2, -0.5, 0.9)
s = [sum(W[i][j] * x0[j] for j in range(3)) for i in range(2)]
D = [[sig(s[i]) * (1 - sig(s[i])) if i == k else 0 for k in range(2)] for i in range(2)]
exact = mm(D, W)
num = jac(layer, x0)
assert all(abs(num[i][j] - exact[i][j]) < 1e-8 for i in range(2) for j in range(3))
print("[OK] 주장 6: 층의 야코비 diag(σ')W")
Z = lambda t: math.cos(t) ** 2 * math.sin(t)
for t in (0.4, 1.7):
d = (Z(t + 1e-6) - Z(t - 1e-6)) / 2e-6
assert abs(d - (-2 * math.cos(t) * math.sin(t) ** 2 + math.cos(t) ** 3)) < 1e-8
print("[OK] 주장 7·카드 C2")
print("ALL CHECKS PASSED")
if __name__ == "__main__":
main()