bs-python-utils 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bs_python_utils/Timer.py +76 -0
- bs_python_utils/__init__.py +0 -0
- bs_python_utils/bs_altair.py +927 -0
- bs_python_utils/bs_logging.py +100 -0
- bs_python_utils/bs_mathstr.py +130 -0
- bs_python_utils/bs_mem.py +148 -0
- bs_python_utils/bs_opt.py +518 -0
- bs_python_utils/bs_plots.py +2 -0
- bs_python_utils/bs_seaborn.py +174 -0
- bs_python_utils/bs_sparse_gaussian.py +46 -0
- bs_python_utils/bsmplutils.py +34 -0
- bs_python_utils/bsnputils.py +957 -0
- bs_python_utils/bssputils.py +79 -0
- bs_python_utils/bsstats.py +463 -0
- bs_python_utils/bsutils.py +363 -0
- bs_python_utils/distance_covariances.py +258 -0
- bs_python_utils/example_opt.py +71 -0
- bs_python_utils/examples_altair.py +195 -0
- bs_python_utils/examples_distance_covariances.py +32 -0
- bs_python_utils/examples_mem.py +25 -0
- bs_python_utils/examples_seaborn.py +37 -0
- bs_python_utils/examples_sklearn.py +33 -0
- bs_python_utils/pandas_utils.py +239 -0
- bs_python_utils/sklearn_utils.py +74 -0
- bs_python_utils-0.0.1.dist-info/LICENSE +21 -0
- bs_python_utils-0.0.1.dist-info/METADATA +71 -0
- bs_python_utils-0.0.1.dist-info/RECORD +28 -0
- bs_python_utils-0.0.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
""" interface to scipy.optimize """
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from math import sqrt
|
|
4
|
+
from typing import Any, Callable, Iterable, Optional, Union, cast
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import scipy.linalg as spla
|
|
8
|
+
import scipy.optimize as spopt
|
|
9
|
+
|
|
10
|
+
from bs_python_utils.bsnputils import TwoArrays, check_vector, npmaxabs
|
|
11
|
+
from bs_python_utils.bssputils import describe_array
|
|
12
|
+
from bs_python_utils.bsutils import bs_error_abort, print_stars
|
|
13
|
+
from bs_python_utils.Timer import timeit
|
|
14
|
+
|
|
15
|
+
ScalarFunctionAndGradient = Callable[
|
|
16
|
+
[np.ndarray, Iterable, Optional[bool]], Union[float, tuple[float, np.ndarray]]
|
|
17
|
+
]
|
|
18
|
+
"""Type of f(v, args, gr) that returns a scalar value and also a gradient if gr is True"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
ProximalFunction = Callable[[np.ndarray, float, Iterable], np.ndarray]
|
|
22
|
+
"""Type of h(x, t, pars) that returns a scalar value"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class OptimizeParams:
|
|
27
|
+
"""
|
|
28
|
+
used for optimization;
|
|
29
|
+
combines values, bounds and initial values for a parameter vector
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
params_values: np.ndarray | None
|
|
33
|
+
params_bounds: list[tuple] | None
|
|
34
|
+
params_init: np.ndarray | None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def print_optimization_results(
|
|
38
|
+
resus: spopt.OptimizeResult, title: str = "Minimizing"
|
|
39
|
+
) -> None:
|
|
40
|
+
"""
|
|
41
|
+
print results from unconstrained optimization
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
resus: results from optimization
|
|
45
|
+
title: a title
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
just prints
|
|
49
|
+
"""
|
|
50
|
+
print_stars(title)
|
|
51
|
+
print(resus.message)
|
|
52
|
+
if resus.success:
|
|
53
|
+
print(f"Successful! in {resus.nit} iterations")
|
|
54
|
+
print(f" evaluated {resus.nfev} functions functions and {resus.njev} gradients")
|
|
55
|
+
print("\nMinimizer and grad_f:")
|
|
56
|
+
print(np.column_stack((resus.x, resus.jac)))
|
|
57
|
+
print(f"Minimized value is {resus.fun}")
|
|
58
|
+
else:
|
|
59
|
+
print_stars("Minimization failed!")
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def print_constrained_optimization_results(
|
|
64
|
+
resus: spopt.OptimizeResult,
|
|
65
|
+
title: str = "Minimizing",
|
|
66
|
+
print_constr: bool = False,
|
|
67
|
+
print_multipliers: bool = False,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""
|
|
70
|
+
print results from constrained optimization
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
resus: results from optimization
|
|
74
|
+
str title: a title
|
|
75
|
+
print_constr: if `True`, print the values of the constraints
|
|
76
|
+
print_multipliers: if `True`, print the values of the multipliers
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
just prints
|
|
80
|
+
"""
|
|
81
|
+
print_stars(title)
|
|
82
|
+
print(resus.message)
|
|
83
|
+
if resus.success:
|
|
84
|
+
print(f"Successful! in {resus.nit} iterations")
|
|
85
|
+
print(f" evaluated {resus.nfev} functions and {resus.njev} gradients")
|
|
86
|
+
print(f"Minimized value is {resus.fun}")
|
|
87
|
+
print(f"The Lagrangian norm is {resus.optimality}")
|
|
88
|
+
print(f"The largest constraint violation is {resus.constr_violation}")
|
|
89
|
+
if print_multipliers:
|
|
90
|
+
print(f"The multipliers are {resus.v}")
|
|
91
|
+
if print_constr:
|
|
92
|
+
print(f"The values of the constraints are {resus.constr}")
|
|
93
|
+
else:
|
|
94
|
+
print_stars("Constrained minimization failed!")
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def armijo_alpha(
|
|
99
|
+
f: Callable,
|
|
100
|
+
x: np.ndarray,
|
|
101
|
+
d: np.ndarray,
|
|
102
|
+
args: Iterable,
|
|
103
|
+
alpha_init: float = 1.0,
|
|
104
|
+
beta: float = 0.5,
|
|
105
|
+
max_iter: int = 100,
|
|
106
|
+
tol: float = 0.0,
|
|
107
|
+
) -> float:
|
|
108
|
+
"""Given a function `f` we are minimizing, computes the step size `alpha`
|
|
109
|
+
to take in the direction `d` using the Armijo rule
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
f: the function
|
|
113
|
+
x: the current point
|
|
114
|
+
d: the direction we are taking
|
|
115
|
+
args: other arguments passed to `f`
|
|
116
|
+
alpha_init: the initial step size
|
|
117
|
+
beta: the step size reduction factor
|
|
118
|
+
max_iter: the maximum number of iterations
|
|
119
|
+
tol: a tolerance
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
the step size alpha
|
|
123
|
+
"""
|
|
124
|
+
f0 = f(x, args)
|
|
125
|
+
alpha = alpha_init
|
|
126
|
+
for _ in range(max_iter):
|
|
127
|
+
x1 = x + alpha * d
|
|
128
|
+
f1 = f(x1, args)
|
|
129
|
+
if f1 < f0 + tol:
|
|
130
|
+
return alpha
|
|
131
|
+
alpha *= beta
|
|
132
|
+
else:
|
|
133
|
+
bs_error_abort("Too many iterations")
|
|
134
|
+
return alpha
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def barzilai_borwein_alpha(
|
|
138
|
+
grad_f: Callable, x: np.ndarray, args: Iterable
|
|
139
|
+
) -> tuple[float, np.ndarray]:
|
|
140
|
+
"""Given a function `f` we are minimizing, computes the step size `alpha`
|
|
141
|
+
to take in the opposite direction of the gradient using the Barzilai-Borwein rule
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
grad_f: the gradient of the function
|
|
145
|
+
x: the current point
|
|
146
|
+
args: other arguments passed to `f`
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
the step size `alpha` and the gradient `g` at the point `x`
|
|
150
|
+
"""
|
|
151
|
+
g = grad_f(x, args)
|
|
152
|
+
alpha = 1.0 / spla.norm(g)
|
|
153
|
+
x_hat = x - alpha * g
|
|
154
|
+
g_hat = grad_f(x_hat, args)
|
|
155
|
+
norm_dg = spla.norm(g - g_hat)
|
|
156
|
+
norm_dg2 = norm_dg * norm_dg
|
|
157
|
+
alpha = np.abs(np.dot(x - x_hat, g - g_hat)) / norm_dg2
|
|
158
|
+
return alpha, g
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def check_gradient_scalar_function(
|
|
162
|
+
fg: ScalarFunctionAndGradient,
|
|
163
|
+
p: np.ndarray,
|
|
164
|
+
args: Iterable,
|
|
165
|
+
mode: str = "central",
|
|
166
|
+
EPS: float = 1e-6,
|
|
167
|
+
) -> TwoArrays:
|
|
168
|
+
"""Checks the gradient of a scalar function
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
fg: should return the scalar value, and the gradient if its `gr` argument is `True`
|
|
172
|
+
p: where we are checking the gradient
|
|
173
|
+
args: other arguments passed to `fg`
|
|
174
|
+
mode: "central" or "forward" derivatives
|
|
175
|
+
EPS: the step for forward or central derivatives
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
the analytic and numeric gradients
|
|
179
|
+
"""
|
|
180
|
+
f0, f_grad = fg(p, args, gr=True) # type: ignore
|
|
181
|
+
f0 = cast(float, f0)
|
|
182
|
+
|
|
183
|
+
print_stars("checking the gradient: analytic, numeric")
|
|
184
|
+
|
|
185
|
+
g = np.zeros_like(p)
|
|
186
|
+
if mode == "central":
|
|
187
|
+
for i, x in enumerate(p):
|
|
188
|
+
p1 = p.copy()
|
|
189
|
+
p1[i] = x + EPS
|
|
190
|
+
f_plus = cast(float, fg(p1, args, gr=False)) # type: ignore
|
|
191
|
+
p1[i] -= 2.0 * EPS
|
|
192
|
+
f_minus = cast(float, fg(p1, args, gr=False)) # type: ignore
|
|
193
|
+
g[i] = (f_plus - f_minus) / (2.0 * EPS)
|
|
194
|
+
print(f"{i}: {f_grad[i]}, {g[i]}")
|
|
195
|
+
elif mode == "forward":
|
|
196
|
+
for i, x in enumerate(p):
|
|
197
|
+
p1 = p.copy()
|
|
198
|
+
p1[i] = x + EPS
|
|
199
|
+
f_plus = cast(float, fg(p1, args, gr=False)) # type: ignore
|
|
200
|
+
g[i] = (f_plus - f0) / EPS
|
|
201
|
+
print(f"{i}: {f_grad[i]}, {g[i]}")
|
|
202
|
+
else:
|
|
203
|
+
bs_error_abort("mode must be 'central' or 'forward'")
|
|
204
|
+
|
|
205
|
+
return f_grad, g
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@timeit
|
|
209
|
+
def acc_grad_descent(
|
|
210
|
+
grad_f: Callable,
|
|
211
|
+
x_init: np.ndarray,
|
|
212
|
+
other_params: Iterable,
|
|
213
|
+
prox_h: ProximalFunction | None = None,
|
|
214
|
+
print_result: bool = False,
|
|
215
|
+
verbose: bool = False,
|
|
216
|
+
tol: float = 1e-9,
|
|
217
|
+
alpha: float = 1.01,
|
|
218
|
+
beta: float = 0.5,
|
|
219
|
+
maxiter: int = 10000,
|
|
220
|
+
) -> tuple[np.ndarray, int]:
|
|
221
|
+
"""
|
|
222
|
+
minimizes `(f+h)` by Accelerated Gradient Descent
|
|
223
|
+
where `f` is smooth and convex and `h` is convex.
|
|
224
|
+
|
|
225
|
+
By default `h` is zero.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
grad_f: grad_f of `f`; should return an `(n)` array from an `(n)` array \
|
|
229
|
+
and the `other_ params` object
|
|
230
|
+
x_init: initial guess, shape `(n)`
|
|
231
|
+
prox_h: proximal projector of `h`, if any
|
|
232
|
+
other_params: an iterable with additional parameters
|
|
233
|
+
verbose: if `True`, print diagnosis
|
|
234
|
+
tol: convergence criterion on absolute grad_f
|
|
235
|
+
alpha: ceiling on step multiplier
|
|
236
|
+
beta: floor on step multiplier
|
|
237
|
+
maxiter: max number of iterations
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
the candidate solution, and 1 if converged/0 if not
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
# no proximal projection if no h
|
|
244
|
+
local_prox_h: ProximalFunction = prox_h if prox_h else lambda x, t, p: x
|
|
245
|
+
|
|
246
|
+
x = x_init.copy()
|
|
247
|
+
y = x_init.copy()
|
|
248
|
+
|
|
249
|
+
# for stepsize we use Barzilai-Borwein
|
|
250
|
+
t, g = barzilai_borwein_alpha(grad_f, y, other_params)
|
|
251
|
+
|
|
252
|
+
grad_err_init = npmaxabs(g)
|
|
253
|
+
|
|
254
|
+
if verbose:
|
|
255
|
+
print(f"agd: grad_err_init={grad_err_init}")
|
|
256
|
+
|
|
257
|
+
n_iter = 0
|
|
258
|
+
theta = 1.0
|
|
259
|
+
|
|
260
|
+
while n_iter < maxiter:
|
|
261
|
+
grad_err = npmaxabs(g)
|
|
262
|
+
if grad_err < tol:
|
|
263
|
+
break
|
|
264
|
+
xi = x
|
|
265
|
+
yi = y
|
|
266
|
+
x = y - t * g
|
|
267
|
+
x = local_prox_h(x, t, other_params)
|
|
268
|
+
|
|
269
|
+
theta = 2.0 / (1.0 + sqrt(1.0 + 4.0 / theta / theta))
|
|
270
|
+
|
|
271
|
+
if np.dot(y - x, x - xi) > 0: # wrong direction, we restart
|
|
272
|
+
x = xi
|
|
273
|
+
y = x
|
|
274
|
+
theta = 1.0
|
|
275
|
+
else:
|
|
276
|
+
y = x + (1.0 - theta) * (x - xi)
|
|
277
|
+
|
|
278
|
+
gi = g
|
|
279
|
+
g = grad_f(y, other_params)
|
|
280
|
+
ndy = spla.norm(y - yi)
|
|
281
|
+
t_hat = 0.5 * ndy * ndy / abs(np.dot(y - yi, gi - g))
|
|
282
|
+
t = min(alpha * t, max(beta * t, t_hat))
|
|
283
|
+
|
|
284
|
+
n_iter += 1
|
|
285
|
+
|
|
286
|
+
if verbose:
|
|
287
|
+
print(f" AGD with grad_err = {grad_err} after {n_iter} iterations")
|
|
288
|
+
|
|
289
|
+
x_conv = y
|
|
290
|
+
|
|
291
|
+
ret_code = 0 if grad_err < tol else 1
|
|
292
|
+
|
|
293
|
+
if verbose or print_result:
|
|
294
|
+
if ret_code == 0:
|
|
295
|
+
print_stars(
|
|
296
|
+
f" AGD converged with grad_err = {grad_err} after {iter} iterations"
|
|
297
|
+
)
|
|
298
|
+
else:
|
|
299
|
+
print_stars(
|
|
300
|
+
f" Problem in AGD: grad_err = {grad_err} after {iter} iterations"
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
return (x_conv, ret_code)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _fix_some(
|
|
307
|
+
obj: Callable, grad_obj: Callable, fixed_vars: list[int], fixed_vals: np.ndarray
|
|
308
|
+
) -> tuple[Callable, Callable]:
|
|
309
|
+
"""
|
|
310
|
+
Takes in a function and its gradient, fixes the variables
|
|
311
|
+
whose indices are `fixed_vars` to the values in `fixed_vals`,
|
|
312
|
+
and returns the modified function and its gradient
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
obj: the original function
|
|
316
|
+
grad_obj: its gradient function
|
|
317
|
+
fixed_vars: a list if the indices of variables whose values are fixed
|
|
318
|
+
fixed_vals: their fixed values
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
the modified function and its modified gradient function
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
def fixed_obj(t, other_args):
|
|
325
|
+
t_full = list(t)
|
|
326
|
+
for i, i_coef in enumerate(fixed_vars):
|
|
327
|
+
t_full.insert(i_coef, fixed_vals[i])
|
|
328
|
+
arr_full = np.array(t_full)
|
|
329
|
+
return obj(arr_full, other_args)
|
|
330
|
+
|
|
331
|
+
def fixed_grad_obj(t, other_args):
|
|
332
|
+
t_full = list(t)
|
|
333
|
+
for i, i_coef in enumerate(fixed_vars):
|
|
334
|
+
t_full.insert(i_coef, fixed_vals[i])
|
|
335
|
+
arr_full = np.array(t_full)
|
|
336
|
+
grad_full = grad_obj(arr_full, other_args)
|
|
337
|
+
return np.delete(grad_full, fixed_vars)
|
|
338
|
+
|
|
339
|
+
return fixed_obj, fixed_grad_obj
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def minimize_some_fixed(
|
|
343
|
+
obj: Callable,
|
|
344
|
+
grad_obj: Callable,
|
|
345
|
+
x_init: np.ndarray,
|
|
346
|
+
args: Iterable,
|
|
347
|
+
fixed_vars: list[int] | None,
|
|
348
|
+
fixed_vals: np.ndarray | None,
|
|
349
|
+
options: dict | None = None,
|
|
350
|
+
bounds: list[tuple[float, float]] | None = None,
|
|
351
|
+
) -> Any:
|
|
352
|
+
"""
|
|
353
|
+
minimize a function with some variables fixed, using L-BFGS-B
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
obj: the original function
|
|
357
|
+
grad_obj: its gradient function
|
|
358
|
+
fixed_vars: a list if the indices of variables whose values are fixed
|
|
359
|
+
fixed_vals: their fixed values
|
|
360
|
+
x_init: the initial values of all variables (those on fixed variables are not used)
|
|
361
|
+
args: other parameters
|
|
362
|
+
options: any options passed on to scipy.optimize.minimize
|
|
363
|
+
bounds: the bounds on all variables (those on fixed variables are not used)
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
the result of optimization, on all variables
|
|
367
|
+
"""
|
|
368
|
+
if fixed_vars is None:
|
|
369
|
+
resopt = spopt.minimize(
|
|
370
|
+
obj,
|
|
371
|
+
x_init,
|
|
372
|
+
method="L-BFGS-B",
|
|
373
|
+
args=args,
|
|
374
|
+
options=options,
|
|
375
|
+
jac=grad_obj,
|
|
376
|
+
bounds=bounds,
|
|
377
|
+
)
|
|
378
|
+
else:
|
|
379
|
+
fixed_vars = cast(list, fixed_vars)
|
|
380
|
+
n_fixed = check_vector(fixed_vals)
|
|
381
|
+
fixed_vals = cast(np.ndarray, fixed_vals)
|
|
382
|
+
if len(fixed_vars) != n_fixed:
|
|
383
|
+
bs_error_abort(
|
|
384
|
+
f"fixed_vars has {len(fixed_vars)} indices but fixed_vals has"
|
|
385
|
+
f" {fixed_vals.size} elements."
|
|
386
|
+
)
|
|
387
|
+
fixed_obj, fixed_grad_obj = _fix_some(obj, grad_obj, fixed_vars, fixed_vals)
|
|
388
|
+
|
|
389
|
+
# drop fixed variables and the corresponding bounds
|
|
390
|
+
n = len(x_init)
|
|
391
|
+
not_fixed = np.ones(n, dtype=bool)
|
|
392
|
+
not_fixed[fixed_vars] = False
|
|
393
|
+
t_init = x_init[not_fixed]
|
|
394
|
+
t_bounds = (
|
|
395
|
+
None if bounds is None else [bounds[i] for i in range(n) if not_fixed[i]]
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
resopt = spopt.minimize(
|
|
399
|
+
fixed_obj,
|
|
400
|
+
t_init,
|
|
401
|
+
method="L-BFGS-B",
|
|
402
|
+
args=args,
|
|
403
|
+
options=options,
|
|
404
|
+
jac=fixed_grad_obj,
|
|
405
|
+
bounds=t_bounds,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
# now re-fill the values of the variables
|
|
409
|
+
t = resopt.x
|
|
410
|
+
t_full = list(t)
|
|
411
|
+
for i, i_coef in enumerate(fixed_vars):
|
|
412
|
+
t_full.insert(i_coef, fixed_vals[i])
|
|
413
|
+
resopt.x = t_full
|
|
414
|
+
|
|
415
|
+
# and re-fill the values of the gradients
|
|
416
|
+
g = grad_obj(np.array(t_full), args)
|
|
417
|
+
resopt.jac = g
|
|
418
|
+
|
|
419
|
+
return resopt
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def dfp_update(
|
|
423
|
+
hess_inv: np.ndarray, gradient_diff: np.ndarray, x_diff: np.ndarray
|
|
424
|
+
) -> np.ndarray:
|
|
425
|
+
"""runs a DFP update for the inverse Hessian
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
hess_inv: the current inverse Hessian
|
|
429
|
+
gradient_diff: the update in the gradient
|
|
430
|
+
x_diff: the update in x
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
the updated inverse Hessian
|
|
434
|
+
"""
|
|
435
|
+
xdt = x_diff.T
|
|
436
|
+
xxp = x_diff @ xdt
|
|
437
|
+
xpg = xdt @ gradient_diff
|
|
438
|
+
hdg = hess_inv @ gradient_diff
|
|
439
|
+
dgp_hdg = gradient_diff.T @ hdg
|
|
440
|
+
hess_inv_new = hess_inv + xxp / xpg - (hdg @ hdg.T) / dgp_hdg
|
|
441
|
+
return cast(np.ndarray, hess_inv_new)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def bfgs_update(
|
|
445
|
+
hess_inv: np.ndarray, gradient_diff: np.ndarray, x_diff: np.ndarray
|
|
446
|
+
) -> np.ndarray:
|
|
447
|
+
"""runs a DFP update for the inverse Hessian
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
hess_inv: the current inverse Hessian
|
|
451
|
+
gradient_diff: the update in the gradient
|
|
452
|
+
x_diff: the update in x
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
the updated inverse Hessian
|
|
456
|
+
"""
|
|
457
|
+
xdt = x_diff.T
|
|
458
|
+
xpg = xdt @ gradient_diff
|
|
459
|
+
hdg = hess_inv @ gradient_diff
|
|
460
|
+
dgp_hdg = gradient_diff.T @ hdg
|
|
461
|
+
u = x_diff / xpg - hdg / dgp_hdg
|
|
462
|
+
hess_inv_new = dfp_update(hess_inv, gradient_diff, x_diff) + dgp_hdg * (u @ u.T)
|
|
463
|
+
return cast(np.ndarray, hess_inv_new)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
if __name__ == "__main__":
|
|
467
|
+
print_stars("Testing acc_grad_descent")
|
|
468
|
+
|
|
469
|
+
def grad_f(x, p):
|
|
470
|
+
xp = x - p[0]
|
|
471
|
+
return 4.0 * xp * xp * xp
|
|
472
|
+
|
|
473
|
+
x_init = np.random.normal(size=10000)
|
|
474
|
+
|
|
475
|
+
p = 1.0
|
|
476
|
+
x_conv, ret_code = acc_grad_descent(
|
|
477
|
+
grad_f, x_init, other_params=np.array([p]), tol=1e-12, verbose=False
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
describe_array(x_conv - p, "x-p should be close to zero")
|
|
481
|
+
|
|
482
|
+
def obj(x, args):
|
|
483
|
+
res = x - args
|
|
484
|
+
return np.sum(res * res)
|
|
485
|
+
|
|
486
|
+
def grad_obj(x, args):
|
|
487
|
+
res = x - args
|
|
488
|
+
return 2.0 * res
|
|
489
|
+
|
|
490
|
+
n = 5
|
|
491
|
+
x_init = np.full(n, 0.5)
|
|
492
|
+
args = np.arange(n)
|
|
493
|
+
bounds = [(-10.0, 10.0) for _ in range(n)]
|
|
494
|
+
|
|
495
|
+
fixed_vars = [1, 3]
|
|
496
|
+
fixed_vals = -np.ones(2)
|
|
497
|
+
|
|
498
|
+
resopt = minimize_some_fixed(
|
|
499
|
+
obj,
|
|
500
|
+
grad_obj,
|
|
501
|
+
x_init,
|
|
502
|
+
args,
|
|
503
|
+
fixed_vars=fixed_vars,
|
|
504
|
+
fixed_vals=fixed_vals,
|
|
505
|
+
bounds=bounds,
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
print(resopt)
|
|
509
|
+
|
|
510
|
+
# test the step routines
|
|
511
|
+
g = grad_obj(x_init, args)
|
|
512
|
+
alpha_a = armijo_alpha(obj, x_init, -g, args)
|
|
513
|
+
print(f"\nArmijo alpha={alpha_a}")
|
|
514
|
+
|
|
515
|
+
alpha_b, g_b = barzilai_borwein_alpha(grad_obj, x_init, args)
|
|
516
|
+
print(f"\nBarzilai-Borwein alpha={alpha_a}")
|
|
517
|
+
print("g and g_b:")
|
|
518
|
+
print(np.column_stack((g, g_b)))
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
""" personal library of Seaborn plots
|
|
2
|
+
"""
|
|
3
|
+
from typing import Callable, cast
|
|
4
|
+
|
|
5
|
+
import matplotlib as mpl
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import seaborn as sns
|
|
10
|
+
|
|
11
|
+
SeabornGraph = tuple[mpl.figure.Figure, mpl.figure.Axes]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def bs_sns_get_legend(g: mpl.axes.Axes) -> mpl.legend.Legend:
|
|
15
|
+
"""
|
|
16
|
+
get the legend object of a Seaborn plot
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
g: the plot object
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
leg: the associated Legend object
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# check axes and find which is have legend
|
|
26
|
+
axs = g.axes
|
|
27
|
+
if isinstance(axs, mpl.axes.Axes): # only one Axes
|
|
28
|
+
leg = axs.get_legend()
|
|
29
|
+
else:
|
|
30
|
+
for ax in axs.flat():
|
|
31
|
+
leg = ax.get_legend()
|
|
32
|
+
if leg is not None:
|
|
33
|
+
break
|
|
34
|
+
# or legend may be on a figure
|
|
35
|
+
if leg is None:
|
|
36
|
+
leg = g._legend
|
|
37
|
+
return leg
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def bs_sns_bar_x_byf(
|
|
41
|
+
df: pd.DataFrame,
|
|
42
|
+
xstr: str,
|
|
43
|
+
fstr: str,
|
|
44
|
+
statistic: Callable = np.mean,
|
|
45
|
+
label_x: str | None = None,
|
|
46
|
+
label_f: str | None = None,
|
|
47
|
+
title: str | None = None,
|
|
48
|
+
) -> SeabornGraph:
|
|
49
|
+
"""make a bar plot of x by f and g
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
df: dataframe, should contain columns `xstr` and `fstr`
|
|
53
|
+
xstr: column name of x
|
|
54
|
+
fstr: column name of f
|
|
55
|
+
statistic: statistic to plot (by default, the mean)
|
|
56
|
+
label_x: label of x
|
|
57
|
+
label_f: label of f
|
|
58
|
+
title: title of plot
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
fig: figure
|
|
62
|
+
ax: axes
|
|
63
|
+
"""
|
|
64
|
+
fig, ax = plt.subplots()
|
|
65
|
+
gbar = sns.barplot(
|
|
66
|
+
x=fstr,
|
|
67
|
+
y=xstr,
|
|
68
|
+
data=df,
|
|
69
|
+
estimator=statistic,
|
|
70
|
+
errcolor="r",
|
|
71
|
+
errwidth=0.75,
|
|
72
|
+
capsize=0.2,
|
|
73
|
+
ax=ax,
|
|
74
|
+
)
|
|
75
|
+
xlab = fstr if label_f is None else label_f
|
|
76
|
+
ylab = xstr if label_x is None else label_x
|
|
77
|
+
ax.set_xlabel(xlab)
|
|
78
|
+
ax.set_ylabel(ylab)
|
|
79
|
+
if title is not None:
|
|
80
|
+
ax.set_title(title)
|
|
81
|
+
return cast(SeabornGraph, gbar)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def bs_sns_bar_x_byfg(
|
|
85
|
+
df: pd.DataFrame,
|
|
86
|
+
xstr: str,
|
|
87
|
+
fstr: str,
|
|
88
|
+
gstr: str,
|
|
89
|
+
statistic: Callable = np.mean,
|
|
90
|
+
label_x: str | None = None,
|
|
91
|
+
label_f: str | None = None,
|
|
92
|
+
label_g: str | None = None,
|
|
93
|
+
title: str | None = None,
|
|
94
|
+
) -> SeabornGraph:
|
|
95
|
+
"""make a bar plot of x by f and g
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
df: dataframe, should contain columns `xstr`, `fstr`, and `gstr`
|
|
99
|
+
xstr: column name of x
|
|
100
|
+
fstr: column name of f
|
|
101
|
+
gstr: column name of g
|
|
102
|
+
statistic: statistic to plot (by default, the mean)
|
|
103
|
+
label_x: label of x
|
|
104
|
+
label_f: label of f
|
|
105
|
+
label_g: label of g in legend
|
|
106
|
+
title: title of plot
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
fig: figure
|
|
110
|
+
ax: axes
|
|
111
|
+
"""
|
|
112
|
+
fig, ax = plt.subplots()
|
|
113
|
+
gbar = sns.barplot(
|
|
114
|
+
x=fstr,
|
|
115
|
+
y=xstr,
|
|
116
|
+
data=df,
|
|
117
|
+
hue=gstr,
|
|
118
|
+
estimator=statistic,
|
|
119
|
+
errcolor="r",
|
|
120
|
+
errwidth=0.75,
|
|
121
|
+
capsize=0.2,
|
|
122
|
+
ax=ax,
|
|
123
|
+
)
|
|
124
|
+
xlab = fstr if label_f is None else label_f
|
|
125
|
+
ylab = xstr if label_x is None else label_x
|
|
126
|
+
ax.set_xlabel(xlab)
|
|
127
|
+
ax.set_ylabel(ylab)
|
|
128
|
+
if label_g is not None:
|
|
129
|
+
gbar_legend = bs_sns_get_legend(gbar)
|
|
130
|
+
gbar_legend.set_title(label_g)
|
|
131
|
+
if title is not None:
|
|
132
|
+
ax.set_title(title)
|
|
133
|
+
return cast(SeabornGraph, gbar)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def bs_sns_density_estimates(
|
|
137
|
+
df: pd.DataFrame,
|
|
138
|
+
true_values: np.ndarray,
|
|
139
|
+
method_string: str | None = "Estimator",
|
|
140
|
+
coeff_string: str | None = "Parameter",
|
|
141
|
+
estimate_string: str | None = "Estimate",
|
|
142
|
+
max_cols: int = 3,
|
|
143
|
+
) -> sns.FacetGrid:
|
|
144
|
+
"""
|
|
145
|
+
plot of the densities of estimates of several coefficients with several methods,
|
|
146
|
+
superposed by methods and faceted by coefficients
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
df: contains columns `method_string`, `coeff_name`, `estimate_value`
|
|
150
|
+
true_values: the true values of the coefficients
|
|
151
|
+
method_string: the name of the column that indicates the method
|
|
152
|
+
coeff_string: the name of the column that indicates the coefficient
|
|
153
|
+
estimate_string: the name of the column that gives the value of the estimate
|
|
154
|
+
max_cols: we wrap after that
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
the `FacetGrid`
|
|
158
|
+
|
|
159
|
+
"""
|
|
160
|
+
g = sns.FacetGrid(
|
|
161
|
+
data=df,
|
|
162
|
+
sharex=False,
|
|
163
|
+
sharey=False,
|
|
164
|
+
hue=method_string,
|
|
165
|
+
col=coeff_string,
|
|
166
|
+
col_wrap=max_cols,
|
|
167
|
+
)
|
|
168
|
+
g.map(sns.kdeplot, estimate_string)
|
|
169
|
+
g.set_titles("{col_name}")
|
|
170
|
+
for true_val, ax in zip(true_values, g.axes.ravel(), strict=True):
|
|
171
|
+
ax.vlines(true_val, *ax.get_ylim(), color="k", linestyles="dashed")
|
|
172
|
+
g.add_legend()
|
|
173
|
+
|
|
174
|
+
return g
|