Skip to content

Commit 7c4188e

Browse files
committed
added fast_cross_validation branch with cross validation for bandwidth detection and example script
1 parent a5722cb commit 7c4188e

3 files changed

Lines changed: 205 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ src/pyNFFT3/lib/AVX2/libwinpthread-1.dll
2020
simpleTest/venv/
2121
bandwidth_detection*
2222
cross_validation*
23+
log/
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# pip install pyANOVAapprox
2+
3+
# Example for approximating an periodic function
4+
5+
import math
6+
7+
import matplotlib.pyplot as plt
8+
import numpy as np
9+
from TestFunctionPeriodic import *
10+
11+
12+
import os
13+
import sys
14+
15+
src_aa = os.path.abspath(os.path.join(os.getcwd(), "src"))
16+
sys.path.insert(0, src_aa)
17+
18+
import pyANOVAapprox as ANOVAapprox
19+
20+
21+
def TestFunction(x):
22+
return b_spline_2(x[0]) * b_spline_4(x[1]) * b_spline_6(x[2])
23+
24+
25+
rng = np.random.default_rng(1234)
26+
27+
##################################
28+
## Definition of the parameters ##
29+
##################################
30+
31+
d = 3 # dimension
32+
33+
M = 100000 # number of used evaluation points to train the model
34+
M_test = 100000 # number of used evaluation points to test the accuracity the model
35+
36+
U = [(), (0,), (1,), (2,), (0, 1), (0, 2), (1, 2), (0, 1, 2)]
37+
38+
lambdas = np.array([0.0]) # used regularisation parameters λ
39+
40+
############################
41+
## Generation of the data ##
42+
############################
43+
44+
X = rng.random((M, d)) # construct the evaluation points for training
45+
y = np.array(
46+
[TestFunction(X[i, :].T) for i in range(M)], dtype=complex
47+
) # evaluate the function at these points
48+
X = X - 0.5
49+
X_test = rng.random((M_test, d))
50+
y_test = np.array(
51+
[TestFunction(X_test[i, :].T) for i in range(M_test)], dtype=complex
52+
) # the same for the test points
53+
X_test = X_test - 0.5
54+
55+
##########################
56+
## Do the approximation ##
57+
##########################
58+
59+
ads = ANOVAapprox.approx(X, y, U=U, basis="per")
60+
ads.autoapproximate(cross_validation=True, verbosity=7)
61+
62+
################################
63+
## get approximation accuracy ##
64+
################################
65+
66+
# mse = ANOVAapprox.get_mse(ads) # get mse error at the given training points
67+
mse = ads.get_mse(X=X_test, y=y_test) # get mse error at the test points
68+
λ_min = min(
69+
mse, key=mse.get
70+
) # get the regularisation parameter which leads to the minimal error
71+
mse_min = mse[λ_min]
72+
73+
print("mse = " + str(mse_min))

src/pyANOVAapprox/approx.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import copy
22
import warnings
3+
import os
4+
import csv
5+
import math
6+
from bisect import bisect
37

48
import pyANOVAapprox.analysis as ANOVAanalysis
59
import pyANOVAapprox.bandwidth as ANOVAbandwidth
@@ -398,6 +402,88 @@ def approximate(
398402

399403
estimate_rates = ANOVAbandwidth.estimate_rates
400404

405+
def _compute_cv_error(
406+
self,
407+
D,
408+
t,
409+
setting,
410+
lam,
411+
solver,
412+
solver_max_iter,
413+
solver_weights,
414+
solver_verbose,
415+
solver_tol,
416+
min_B_step=2,
417+
cv_max_iter=20,
418+
verbosity=0,
419+
):
420+
"""
421+
Computes GCV error for a fixed budget B.
422+
423+
If optimize_budget=True, performs a binary-search style minimization over
424+
[B_min, B_max] and returns (best_B, best_cv_error).
425+
Otherwise returns cv_error for the provided B.
426+
"""
427+
428+
def _cv_for_budget(B_curr):
429+
bw = compute_bandwidth(B_curr, D, t)
430+
431+
# Create temporary setting for this bandwidth
432+
temp_setting = copy.copy(setting)
433+
temp_setting.N = [bw[i] for i in setting.U]
434+
435+
# Save active setting and attach temporary objects
436+
self.addSetting(temp_setting)
437+
settingnr_temp = self.aktsetting
438+
self.addTrafo(settingnr_temp)
439+
440+
441+
self._approximate(
442+
lam,
443+
settingnr=settingnr_temp,
444+
max_iter=solver_max_iter,
445+
weights=solver_weights,
446+
verbose=solver_verbose,
447+
solver=solver,
448+
tol=solver_tol,
449+
)
450+
451+
M = self.X.shape[0]
452+
nfreqs = np.sum([np.prod(np.array(bw[u])-1) for u in temp_setting.U])+1
453+
return 1/M * np.linalg.norm(self.evaluate(settingnr=settingnr_temp, lam=lam) - self.y)**2/(1-nfreqs/M)**2
454+
455+
B_min = float(max(sum(5 ** len(u) for u in setting.U), 1))
456+
B_max = float(max(B_min + 1.0, len(self.y) / 3.0))
457+
458+
# Binary-search style minimization using local slope sign from finite differences.
459+
for cv_iter in range(cv_max_iter):
460+
mid = 0.5 * (B_min + B_max)
461+
delta = max(1.0, 0.01 * mid)
462+
left_B = max(B_min, mid - delta)
463+
right_B = min(B_max, mid + delta)
464+
465+
cv_left = _cv_for_budget(left_B)
466+
cv_right = _cv_for_budget(right_B)
467+
468+
if verbosity > 4:
469+
print(
470+
f" CV iteration {cv_iter + 1}: "
471+
f"B_left={left_B:.2f}, B_right={right_B:.2f}, "
472+
f"cv_left={cv_left:.6e}, cv_right={cv_right:.6e}"
473+
)
474+
475+
if cv_right < cv_left:
476+
B_min = mid
477+
else:
478+
B_max = mid
479+
480+
if (B_max - B_min) <= min_B_step:
481+
break
482+
483+
best_B = 0.5 * (B_min + B_max)
484+
best_cv = _cv_for_budget(best_B)
485+
return best_B, best_cv
486+
401487
def _autoapproximate(
402488
self,
403489
lam,
@@ -410,6 +496,9 @@ def _autoapproximate(
410496
solver_weights,
411497
solver_verbose,
412498
solver_tol,
499+
cross_validation,
500+
min_B_step,
501+
cv_max_iter,
413502
):
414503
settingnrs = []
415504
setting = self.getSetting(settingnr)
@@ -430,6 +519,29 @@ def _autoapproximate(
430519
for idx in range(maxiter):
431520
if verbosity > 0:
432521
print("===== Iteration ", str(idx + 1), " =====")
522+
523+
# Optional: Use cross-validation to find optimal B
524+
if cross_validation:
525+
if verbosity > 0:
526+
print("Running binary search for optimal bandwidth via cross-validation...")
527+
528+
B, cv_val = self._compute_cv_error(
529+
D=D,
530+
t=t,
531+
setting=copy.copy(setting),
532+
lam=lam,
533+
solver=solver,
534+
solver_max_iter=solver_max_iter,
535+
solver_weights=solver_weights,
536+
solver_verbose=solver_verbose,
537+
solver_tol=solver_tol,
538+
min_B_step=min_B_step,
539+
cv_max_iter=cv_max_iter,
540+
verbosity=verbosity,
541+
)
542+
if verbosity > 0:
543+
print(f"Optimal B from CV: {B:.1f} (cv={cv_val:.6e})")
544+
433545
bw = compute_bandwidth(B, D, t)
434546
if setting.N is not None:
435547
self.addSetting(setting)
@@ -490,7 +602,23 @@ def autoapproximate(
490602
solver_weights=None,
491603
solver_verbose=False,
492604
solver_tol=1e-8,
605+
cross_validation=False,
606+
min_B_step=2,
607+
cv_max_iter=20,
493608
):
609+
"""
610+
Automatic approximation with optional cross-validation for bandwidth selection.
611+
612+
Parameters:
613+
-----------
614+
use_cross_validation : bool, optional
615+
If True, uses cross-validation with binary search to find optimal bandwidth B
616+
in each iteration. Default is False.
617+
cv_tol : float, optional
618+
Convergence tolerance for cross-validation binary search. Default is 1e-3.
619+
cv_max_iter : int, optional
620+
Maximum number of iterations for cross-validation binary search. Default is 20.
621+
"""
494622
settingnr = self.getSettingNr(settingnr)
495623
setting = self.getSetting(settingnr)
496624

@@ -515,6 +643,9 @@ def autoapproximate(
515643
solver_weights=solver_weights,
516644
solver_verbose=solver_verbose,
517645
solver_tol=solver_tol,
646+
cross_validation=cross_validation,
647+
min_B_step=min_B_step,
648+
cv_max_iter=cv_max_iter,
518649
)
519650
self.lam[l] = self.lam[l] + settingnrs
520651

0 commit comments

Comments
 (0)