Skip to content

Commit 4b567f8

Browse files
Merge pull request #22 from numericalalgorithmsgroup/dev_regularized
Nonsmooth regularizers
2 parents ae0399f + 6fc973f commit 4b567f8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+2706
-1026
lines changed

README.rst

Lines changed: 12 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,15 @@ See manual.pdf or `here <https://numericalalgorithmsgroup.github.io/dfols/>`_.
3434

3535
Citation
3636
--------
37-
If you use DFO-LS in a paper, please cite:
37+
The development of DFO-LS is outlined over several publications:
3838

39-
Cartis, C., Fiala, J., Marteau, B. and Roberts, L., `Improving the Flexibility and Robustness of Model-Based Derivative-Free Optimization Solvers <https://doi.org/10.1145/3338517>`_, *ACM Transactions on Mathematical Software*, 45:3 (2019), pp. 32:1-32:41.
39+
1. C. Cartis, J. Fiala, B. Marteau and L. Roberts, `Improving the Flexibility and Robustness of Model-Based Derivative-Free Optimization Solvers <https://doi.org/10.1145/3338517>`_, *ACM Transactions on Mathematical Software*, 45:3 (2019), pp. 32:1-32:41 [`preprint <https://arxiv.org/abs/1804.00154>`_] .
40+
2. M. Hough, and L. Roberts, `Model-Based Derivative-Free Methods for Convex-Constrained Optimization <https://doi.org/10.1137/21M1460971>`_, *SIAM Journal on Optimization*, 21:4 (2022), pp. 2552-2579 [`preprint <https://arxiv.org/abs/2111.05443>`_].
41+
3. Y. Liu, K. H. Lam and L. Roberts, `Black-box Optimization Algorithms for Regularized Least-squares Problems <http://arxiv.org/abs/2407.14915>`_, *arXiv preprint arXiv:arXiv:2407.14915*, 2024.
4042

41-
If you use DFO-LS for problems with constraints, including bound constraints, please also cite:
42-
43-
Hough, M. and Roberts, L., `Model-Based Derivative-Free Methods for Convex-Constrained Optimization <https://doi.org/10.1137/21M1460971>`_, *SIAM Journal on Optimization*, 21:4 (2022), pp. 2552-2579.
43+
If you use DFO-LS in a paper, please cite [1].
44+
If your problem has constraints, including bound constraints, please cite [1,2].
45+
If your problem includes a regularizer, please cite [1,3].
4446

4547
Requirements
4648
------------
@@ -70,27 +72,13 @@ For easy installation, use `pip <http://www.pip-installer.org/>`_ as root:
7072

7173
.. code-block:: bash
7274
73-
$ [sudo] pip install DFO-LS
74-
75-
or alternatively *easy_install*:
76-
77-
.. code-block:: bash
78-
79-
$ [sudo] easy_install DFO-LS
80-
81-
If you do not have root privileges or you want to install DFO-LS for your private use, you can use:
82-
83-
.. code-block:: bash
84-
85-
$ pip install --user DFO-LS
86-
87-
which will install DFO-LS in your home directory.
75+
$ pip install DFO-LS
8876
8977
Note that if an older install of DFO-LS is present on your system you can use:
9078

9179
.. code-block:: bash
9280
93-
$ [sudo] pip install --upgrade DFO-LS
81+
$ pip install --upgrade DFO-LS
9482
9583
to upgrade DFO-LS to the latest version.
9684

@@ -107,22 +95,14 @@ DFO-LS is written in pure Python and requires no compilation. It can be installe
10795

10896
.. code-block:: bash
10997
110-
$ [sudo] pip install .
111-
112-
If you do not have root privileges or you want to install DFO-LS for your private use, you can use:
113-
114-
.. code-block:: bash
115-
116-
$ pip install --user .
117-
118-
instead.
98+
$ pip install .
11999
120100
To upgrade DFO-LS to the latest version, navigate to the top-level directory (i.e. the one containing :code:`pyproject.toml`) and rerun the installation using :code:`pip`, as above:
121101

122102
.. code-block:: bash
123103
124104
$ git pull
125-
$ [sudo] pip install . # with admin privileges
105+
$ pip install .
126106
127107
Testing
128108
-------
@@ -145,7 +125,7 @@ If DFO-LS was installed using *pip* you can uninstall as follows:
145125

146126
.. code-block:: bash
147127
148-
$ [sudo] pip uninstall DFO-LS
128+
$ pip uninstall DFO-LS
149129
150130
If DFO-LS was installed manually you have to remove the installed files by hand (located in your python site-packages directory).
151131

dfols/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
from __future__ import absolute_import, division, print_function, unicode_literals
4040

4141
# DFO-LS version
42-
__version__ = '1.4.1'
42+
__version__ = '1.5.0'
4343

4444
# Main solver & exit flags
4545
from .solver import *

dfols/controller.py

Lines changed: 136 additions & 45 deletions
Large diffs are not rendered by default.

dfols/model.py

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,16 @@
3636
import scipy.linalg as LA
3737

3838
from .trust_region import trsbox_geometry
39-
from .util import sumsq, dykstra
39+
from .util import sumsq, dykstra, remove_scaling
4040

4141
__all__ = ['Model']
4242

4343
module_logger = logging.getLogger(__name__)
4444

4545

4646
class Model(object):
47-
def __init__(self, npt, x0, r0, xl, xu, projections, r0_nsamples, n=None, m=None, abs_tol=1e-12, rel_tol=1e-20, precondition=True,
48-
do_logging=True):
47+
def __init__(self, npt, x0, r0, xl, xu, projections, r0_nsamples, h=None, argsh=(), n=None, m=None, abs_tol=1e-12, rel_tol=1e-20, precondition=True,
48+
do_logging=True, scaling_changes=None):
4949
if n is None:
5050
n = len(x0)
5151
if m is None:
@@ -56,11 +56,15 @@ def __init__(self, npt, x0, r0, xl, xu, projections, r0_nsamples, n=None, m=None
5656
assert xu.shape == (n,), "xu has wrong shape (got %s, expect (%g,))" % (str(xu.shape), n)
5757
assert r0.shape == (m,), "r0 has wrong shape (got %s, expect (%g,))" % (str(r0.shape), m)
5858
self.do_logging = do_logging
59+
self.scaling_changes = scaling_changes
5960
self.dim = n
6061
self.resid_dim = m
6162
self.num_pts = npt
6263
self.npt_so_far = 1 # number of points added so far (with function values)
6364

65+
self.h = h
66+
self.argsh = argsh
67+
6468
# Initialise to blank some useful stuff
6569
# Interpolation points
6670
self.xbase = x0.copy()
@@ -72,12 +76,15 @@ def __init__(self, npt, x0, r0, xl, xu, projections, r0_nsamples, n=None, m=None
7276
# Function values
7377
self.fval_v = np.inf * np.ones((npt, m)) # residuals for each xpt
7478
self.fval_v[0, :] = r0.copy()
75-
self.fval = np.inf * np.ones((npt, )) # overall objective value for each xpt
76-
self.fval[0] = sumsq(r0)
79+
80+
self.objval = np.inf * np.ones((npt, )) # overall objective value for each xpt
81+
self.objval[0] = sumsq(r0)
82+
if h is not None:
83+
self.objval[0] += h(remove_scaling(x0, self.scaling_changes), *argsh)
7784
self.kopt = 0 # index of current iterate (should be best value so far)
7885
self.nsamples = np.zeros((npt,), dtype=int) # number of samples used to evaluate objective at each point
7986
self.nsamples[0] = r0_nsamples
80-
self.fbeg = self.fval[0] # f(x0), saved to check for sufficient reduction
87+
self.objbeg = self.objval[0] # f(x0), saved to check for sufficient reduction
8188

8289
# Termination criteria
8390
self.abs_tol = abs_tol
@@ -90,7 +97,7 @@ def __init__(self, npt, x0, r0, xl, xu, projections, r0_nsamples, n=None, m=None
9097
# Saved point (in absolute coordinates) - always check this value before quitting solver
9198
self.xsave = None
9299
self.rsave = None
93-
self.fsave = None
100+
self.objsave = None
94101
self.jacsave = None
95102
self.nsamples_save = None
96103

@@ -118,8 +125,8 @@ def xopt(self, abs_coordinates=False):
118125
def ropt(self):
119126
return self.fval_v[self.kopt, :] # residuals for current iterate
120127

121-
def fopt(self):
122-
return self.fval[self.kopt]
128+
def objopt(self):
129+
return self.objval[self.kopt]
123130

124131
def xpt(self, k, abs_coordinates=False):
125132
assert 0 <= k < self.npt(), "Invalid index %g" % k
@@ -135,9 +142,9 @@ def rvec(self, k):
135142
assert 0 <= k < self.npt(), "Invalid index %g" % k
136143
return self.fval_v[k, :]
137144

138-
def fval(self, k):
145+
def objval(self, k):
139146
assert 0 <= k < self.npt(), "Invalid index %g" % k
140-
return self.fval[k]
147+
return self.objval[k]
141148

142149
def as_absolute_coordinates(self, x, full_dykstra=False):
143150
# If x were an interpolation point, get the absolute coordinates of x
@@ -177,18 +184,20 @@ def change_point(self, k, x, rvec, allow_kopt_update=True):
177184

178185
self.points[k, :] = x.copy()
179186
self.fval_v[k, :] = rvec.copy()
180-
self.fval[k] = sumsq(rvec)
187+
self.objval[k] = sumsq(rvec)
188+
if self.h is not None:
189+
self.objval[k] += self.h(remove_scaling(self.xbase + x, self.scaling_changes), *self.argsh)
181190
self.nsamples[k] = 1
182191
self.factorisation_current = False
183192

184-
if allow_kopt_update and self.fval[k] < self.fopt():
193+
if allow_kopt_update and self.objval[k] < self.objopt():
185194
self.kopt = k
186195
return
187196

188197
def swap_points(self, k1, k2):
189198
self.points[[k1, k2], :] = self.points[[k2, k1], :]
190199
self.fval_v[[k1, k2], :] = self.fval_v[[k2, k1], :]
191-
self.fval[[k1, k2]] = self.fval[[k2, k1]]
200+
self.objval[[k1, k2]] = self.objval[[k2, k1]]
192201
if self.kopt == k1:
193202
self.kopt = k2
194203
elif self.kopt == k2:
@@ -201,22 +210,27 @@ def add_new_sample(self, k, rvec_extra):
201210
assert 0 <= k < self.npt(), "Invalid index %g" % k
202211
t = float(self.nsamples[k]) / float(self.nsamples[k] + 1)
203212
self.fval_v[k, :] = t * self.fval_v[k, :] + (1 - t) * rvec_extra
204-
self.fval[k] = sumsq(self.fval_v[k, :])
213+
# NOTE: how to sample when we have h? still at xpt(k), then add h(xpt(k)). Modify test if incorrect!
214+
self.objval[k] = sumsq(self.fval_v[k, :])
215+
if self.h is not None:
216+
self.objval[k] += self.h(remove_scaling(self.xbase + self.points[k, :], self.scaling_changes), *self.argsh)
205217
self.nsamples[k] += 1
206218

207-
self.kopt = np.argmin(self.fval[:self.npt()]) # make sure kopt is always the best value we have
219+
self.kopt = np.argmin(self.objval[:self.npt()]) # make sure kopt is always the best value we have
208220
return
209221

210222
def add_new_point(self, x, rvec):
211223
self.points = np.append(self.points, x.reshape((1, self.n())), axis=0) # append row to xpt
212224
self.fval_v = np.append(self.fval_v, rvec.reshape((1, self.m())), axis=0) # append row to fval_v
213-
f = np.dot(rvec, rvec)
214-
self.fval = np.append(self.fval, f) # append entry to fval
225+
obj = sumsq(rvec)
226+
if self.h is not None:
227+
obj += self.h(remove_scaling(self.xbase + x, self.scaling_changes), *self.argsh)
228+
self.objval = np.append(self.objval, obj) # append entry to fval
215229
self.nsamples = np.append(self.nsamples, 1) # add new sample number
216230
self.num_pts += 1 # make sure npt is updated
217231
self.npt_so_far += 1
218232

219-
if f < self.fopt():
233+
if obj < self.objopt():
220234
self.kopt = self.npt() - 1
221235

222236
self.factorisation_current = False
@@ -236,27 +250,30 @@ def shift_base(self, xbase_shift):
236250
return
237251

238252
def save_point(self, x, rvec, nsamples, x_in_abs_coords=True):
239-
f = sumsq(rvec)
240-
if self.fsave is None or f <= self.fsave:
241-
self.xsave = x.copy() if x_in_abs_coords else self.as_absolute_coordinates(x)
253+
xabs = x.copy() if x_in_abs_coords else self.as_absolute_coordinates(x)
254+
obj = sumsq(rvec)
255+
if self.h is not None:
256+
obj += self.h(remove_scaling(xabs, self.scaling_changes), *self.argsh)
257+
if self.objsave is None or obj <= self.objsave:
258+
self.xsave = xabs
242259
self.rsave = rvec.copy()
243-
self.fsave = f
260+
self.objsave = obj
244261
self.jacsave = self.model_jac.copy()
245262
self.nsamples_save = nsamples
246263
return True
247264
else:
248265
return False # this value is worse than what we have already - didn't save
249266

250267
def get_final_results(self):
251-
# Return x and fval for optimal point (either from xsave+fsave or kopt)
252-
if self.fsave is None or self.fopt() <= self.fsave: # optimal has changed since xsave+fsave were last set
253-
return self.xopt(abs_coordinates=True).copy(), self.ropt().copy(), self.fopt(), self.model_jac.copy(), self.nsamples[self.kopt]
268+
# Return x and objval for optimal point (either from xsave+objsave or kopt)
269+
if self.objsave is None or self.objopt() <= self.objsave: # optimal has changed since xsave+objsave were last set
270+
return self.xopt(abs_coordinates=True).copy(), self.ropt().copy(), self.objopt(), self.model_jac.copy(), self.nsamples[self.kopt]
254271
else:
255-
return self.xsave.copy(), self.rsave.copy(), self.fsave, self.jacsave, self.nsamples_save
272+
return self.xsave.copy(), self.rsave.copy(), self.objsave, self.jacsave, self.nsamples_save
256273

257274
def min_objective_value(self):
258275
# Get termination criterion for f small: f <= abs_tol or f <= rel_tol * f0
259-
return max(self.abs_tol, self.rel_tol * self.fbeg)
276+
return max(self.abs_tol, self.rel_tol * self.objbeg)
260277

261278
def model_value(self, d, d_based_at_xopt=True, with_const_term=False):
262279
if d_based_at_xopt:
@@ -375,7 +392,7 @@ def interpolate_mini_models_svd(self, verbose=False, make_full_rank=False, min_s
375392
return True, interp_error, sqrt(norm_J_error), linalg_resid, ls_interp_cond_num # flag ok
376393

377394
def build_full_model(self):
378-
# Build full least squares objective model from mini-models
395+
# Build full least squares model from mini-models
379396
# Centred around xopt
380397
r = self.model_const + np.dot(self.model_jac, self.xopt()) # constant term (for inexact interpolation)
381398
J = self.model_jac

dfols/params.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def __init__(self, n, npt, maxfun, objfun_has_noise=False):
8282
self.params["restarts.use_soft_restarts"] = True
8383
self.params["restarts.soft.num_geom_steps"] = 3
8484
self.params["restarts.soft.move_xk"] = True
85-
self.params["restarts.soft.max_fake_successful_steps"] = maxfun # number ratio>0 steps below fsave allowed
85+
self.params["restarts.soft.max_fake_successful_steps"] = maxfun # number ratio>0 steps below objsave allowed
8686
self.params["restarts.hard.use_old_rk"] = True # recycle r(xk) from previous run?
8787
self.params["restarts.increase_npt"] = False
8888
self.params["restarts.increase_npt_amt"] = 1
@@ -109,12 +109,20 @@ def __init__(self, n, npt, maxfun, objfun_has_noise=False):
109109
self.params["growing.full_rank.min_sing_val"] = 1e-6 # absolute floor on singular values
110110
self.params["growing.full_rank.svd_max_jac_cond"] = 1e8 # maximum condition number of Jacobian
111111
self.params["growing.perturb_trust_region_step"] = False # add random direction onto TRS solution?
112+
112113
# Dykstra's algorithm
113114
self.params["dykstra.d_tol"] = 1e-10
114115
self.params["dykstra.max_iters"] = 100
116+
115117
# Matrix rank algorithm
116118
self.params["matrix_rank.r_tol"] = 1e-18
117-
119+
120+
# Function tolerance when applying S-FISTA method
121+
self.params["func_tol.criticality_measure"] = 1e-3
122+
self.params["func_tol.tr_step"] = 1-1e-1
123+
self.params["func_tol.max_iters"] = 500
124+
self.params["sfista.max_iters_scaling"] = 2.0
125+
118126
self.params_changed = {}
119127
for p in self.params:
120128
self.params_changed[p] = False
@@ -268,6 +276,14 @@ def param_type(self, key, npt):
268276
type_str, nonetype_ok, lower, upper = 'int', False, 0, None
269277
elif key == "matrix_rank.r_tol":
270278
type_str, nonetype_ok, lower, upper = 'float', False, 0.0, None
279+
elif key == "func_tol.criticality_measure":
280+
type_str, nonetype_ok, lower, upper = 'float', False, 0.0, 1.0
281+
elif key == "func_tol.tr_step":
282+
type_str, nonetype_ok, lower, upper = 'float', False, 0.0, 1.0
283+
elif key == "func_tol.max_iters":
284+
type_str, nonetype_ok, lower, upper = 'int', False, 0, None
285+
elif key == "sfista.max_iters_scaling":
286+
type_str, nonetype_ok, lower, upper = 'float', False, 1.0, None
271287
else:
272288
assert False, "ParameterList.param_type() has unknown key: %s" % key
273289
return type_str, nonetype_ok, lower, upper

0 commit comments

Comments
 (0)