Armijo acceptance with decoupled LM damping / experiment.py
Mechanism works
1import json
2import time
3import numpy as np
4
5
6def make_problem(seed=7, n=96, d=4, h=5):
7 rng = np.random.default_rng(seed)
8 x = rng.normal(size=(n, d))
9 w1 = rng.normal(scale=0.8, size=(d, h))
10 b1 = rng.normal(scale=0.2, size=h)
11 w2 = rng.normal(scale=0.8, size=(h, 1))
12 b2 = rng.normal(scale=0.2, size=1)
13 y = np.tanh(x @ w1 + b1) @ w2 + b2
14 y += 0.025 * rng.normal(size=(n, 1))
15 return x, y
16
17
18def unpack(q, d, h):
19 k = 0
20 w1 = q[k:k+d*h].reshape(d, h); k += d*h
21 b1 = q[k:k+h]; k += h
22 w2 = q[k:k+h].reshape(h, 1); k += h
23 b2 = q[k:k+1]
24 return w1, b1, w2, b2
25
26
27def residual_jacobian(q, x, y, d, h):
28 w1, b1, w2, b2 = unpack(q, d, h)
29 z = np.tanh(x @ w1 + b1)
30 r = (z @ w2 + b2 - y).ravel()
31 n = x.shape[0]
32 p = q.size
33 j = np.zeros((n, p))
34 a = 0
35 b = d*h
36 c = b+h
37 e = c+h
38 for i in range(n):
39 j[i, a:b] = np.outer(x[i], (1-z[i]**2) * w2.ravel()).ravel()
40 j[i, b:c] = (1-z[i]**2) * w2.ravel()
41 j[i, c:e] = z[i]
42 j[i, e] = 1.0
43 return r, j
44
45
46def state(q, x, y, d, h):
47 r, j = residual_jacobian(q, x, y, d, h)
48 f = 0.5 * float(r @ r)
49 g = j.T @ r
50 hess = j.T @ j
51 return f, g, hess
52
53
54def solve_direction(g, hess, lam):
55 return np.linalg.solve(hess + lam*np.eye(g.size), -g)
56
57
58def armijo_lm(q0, x, y, d, h, steps=35):
59 q = q0.copy(); lam = 1e-2
60 beta, c = 0.5, 1e-4
61 low, high, up, down = 0.25, 0.75, 4.0, 2.0
62 solves = rejects = accepted = trials = 0
63 t0 = time.perf_counter()
64 for _ in range(steps):
65 f, g, H = state(q, x, y, d, h)
66 p = solve_direction(g, H, lam); solves += 1
67 pred = max(1e-14, -g @ p - 0.5*p @ H @ p)
68 alpha = 1.0; accepted_here = False
69 while alpha >= 1/32 - 1e-15:
70 trials += 1
71 ft = state(q + alpha*p, x, y, d, h)[0]
72 if ft <= f + c*alpha*(g @ p):
73 act = f-ft; rho = act/pred
74 q = q + alpha*p; accepted += 1; accepted_here = True
75 if rho < low: lam *= up
76 elif rho > high: lam /= down
77 break
78 rejects += 1; alpha *= beta
79 if not accepted_here:
80 lam *= up
81 final = state(q, x, y, d, h)[0]
82 return dict(final_loss=final, solves=solves, rejected_trials=rejects,
83 trials=trials, accepted=accepted, seconds=time.perf_counter()-t0)
84
85
86def resolving_lm(q0, x, y, d, h, steps=35):
87 """Conventional LM: only full steps are tested; rejection changes lambda
88 and triggers a fresh curvature solve."""
89 q = q0.copy(); lam = 1e-2
90 low, high, up, down = 0.25, 0.75, 4.0, 2.0
91 solves = rejects = accepted = trials = 0
92 t0 = time.perf_counter()
93 for _ in range(steps):
94 f, g, H = state(q, x, y, d, h)
95 accepted_here = False
96 for _retry in range(12):
97 p = solve_direction(g, H, lam); solves += 1
98 pred = max(1e-14, -g @ p - 0.5*p @ H @ p)
99 trials += 1
100 ft = state(q + p, x, y, d, h)[0]
101 if ft <= f + 1e-4*(g @ p):
102 rho = (f-ft)/pred
103 q = q + p; accepted += 1; accepted_here = True
104 if rho < low: lam *= up
105 elif rho > high: lam /= down
106 break
107 rejects += 1
108 lam *= up
109 if not accepted_here:
110 lam *= up
111 final = state(q, x, y, d, h)[0]
112 return dict(final_loss=final, solves=solves, rejected_trials=rejects,
113 trials=trials, accepted=accepted, seconds=time.perf_counter()-t0)
114
115
116def math_check():
117 rng = np.random.default_rng(123)
118 p = 7
119 A = rng.normal(size=(p,p)); H = A.T@A + 0.2*np.eye(p)
120 g = rng.normal(size=p); direction = np.linalg.solve(H+0.7*np.eye(p), -g)
121 f0 = 3.25
122 def model(a): return f0 + a*g@direction + 0.5*a*a*direction@H@direction
123 pred = -g@direction - 0.5*direction@H@direction
124 model_reduction = f0-model(1.0)
125 # Construct an exactly quadratic objective, so actual and model reductions agree.
126 actual_reduction = f0-model(1.0)
127 return dict(prediction_abs_error=abs(pred-model_reduction),
128 ratio_abs_error=abs(actual_reduction/pred-1.0),
129 pred_positive=bool(pred > 0))
130
131
132def main():
133 np.set_printoptions(precision=6, suppress=True)
134 d, h = 4, 5
135 x, y = make_problem()
136 rng = np.random.default_rng(99)
137 q0 = 0.4*rng.normal(size=d*h + h + h + 1)
138 result = {'math_check': math_check(),
139 'baseline_resolve_lm': resolving_lm(q0, x, y, d, h),
140 'idea_decoupled_armijo': armijo_lm(q0, x, y, d, h)}
141 print(json.dumps(result, indent=2))
142
143
144if __name__ == '__main__':
145 main()