2.3. Optimization
Consider the optimization problem:
\[
\min_{x \in \mathbb{R}^n} f(x),
\]
where \(f:\mathbb{R}^n \to \mathbb{R}\) is convex.
To solve the problem numerically, the subpackage specular.optimization.solver provides the following methods:
- the specular gradient (SPEG) method
- the stochastic specular gradient (S-SPEG) method
- the hybrid specular gradient (H-SPEG) method
Given an initial point \(x_0 \in \mathbb{R}^n\), method takes the form:
\[
x_{k+1} = x_k - h_k s_k,
\]
where \(h_k > 0\) is the step size and \(s_k\) is the specular gradient for each \(k \in \mathbb{N}\).
| Name |
Rule (Formula) |
Type |
Input |
Description |
constant |
\(h_k = a\) |
float |
a |
Fixed step size for all \(k\). |
not_summable |
\(h_k = a / \sqrt{k}\) |
float |
a |
\(\lim_{k \to \infty }h_k = 0\), but \(\sum h_k = \infty\). |
square_summable_not_summable |
\(h_k = a / (b + k)\) |
list |
[a, b] |
\(\sum h_k^2 < \infty\) and \(\sum h_k = \infty\). |
geometric_series |
\(h_k = a \cdot r^k\) |
list |
[a, r] |
Exponentially decaying step size. |
user_defined |
Custom |
Callable |
f(k) |
User-provided function of iteration \(k\). |
Quick Example
from specular.optimization.step_size import StepSize
# Use a square-summable rule: h_k = 10 / (2 + k)
step = StepSize(name='square_summable_not_summable', parameters=[10.0, 2.0])
h_1 = step(1)
print(h_1)
2.3.2. The specular gradient method
The one dimensional case
import specular
# Objective function: f(x) = |x|
def f(x):
return abs(x)
step_size = specular.StepSize('constant', 0.1)
# Specular gradient method
res = specular.gradient_method(f=f, x_0=1.0, step_size=step_size, form='specular gradient', max_iter=20)
Higher dimensional cases
import specular
# Objective function: f(x) = sum(x^2)
def f(x):
return float(np.sum(np.array(x)**2))
# Component functions for Stochastic test
# f(x) = x1^2 + x2^2
# f1(x) = x1^2, f2(x) = x2^2
def f_comp_1(x):
return x[0]**2
def f_comp_2(x):
return x[1]**2
f_components = [f_comp_1, f_comp_2]
x_0 = [1.0, 1.0]
step_size = specular.StepSize('square_summable_not_summable', [0.5, 1.0])
# Specular gradient method
res1 = specular.gradient_method(f=f, x_0=x_0, step_size=step_size, form='specular gradient', max_iter=50)
# Stochastic specular gradient method
res2 = specular.gradient_method(f=f, x_0=x_0, step_size=step_size, form='stochastic', f_j=f_components, max_iter=100)
# hybrid specular gradient method
res3 = specular.gradient_method(f=f_quad_vector, x_0=x_0, step_size=step_size, form='hybrid', f_j=f_components, switch_iter=5, max_iter=20)
2.3.3. OptimizationResult
The class OptimizationResult collects the optimization results.
To get history of optimization, call history().
import specular
# Objective function: f(x) = sum(x^2)
def f(x):
return float(np.sum(np.array(x)**2))
x_0 = [1.0, 1.0]
step_size = specular.StepSize('square_summable_not_summable', [0.5, 1.0])
# Specular gradient method
res_x, res_f, res_time = specular.gradient_method(f=f, x_0=x_0, step_size=step_size, form='specular gradient', max_iter=50).history()
2.3.4. API Reference
specular.optimization.step_size.StepSize
Step size rules for optimization methods.
Source code in specular\optimization\step_size.py
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174 | class StepSize:
"""
Step size rules for optimization methods.
"""
__options__ = [
'constant',
'not_summable',
'square_summable_not_summable',
'geometric_series',
'user_defined'
]
def __init__(
self,
name: str,
parameters: float | np.floating | int | Tuple | list | np.ndarray | Callable
):
"""
The step size rules for optimization methods $x_{k+1} = x_k - h_k s_k$, where $s_k$ is the search direction and $h_k > 0$ is the step size at iteration $k >= 1$.
Parameters:
name (str):
Options: 'constant', 'not_summable', 'square_summable_not_summable', 'geometric_series', 'user_defined'
parameters (float | int | tuple | list | np.ndarray | Callable):
The parameters required for the selected step size rule:
* 'constant': float or int
A number `a > 0` for the rule :math:`h_k = a` for each `k`.
* 'not_summable': float or int
A number `a > 0` for the rule :math:`h_k = a / sqrt{k}` for each `k`.
* 'square_summable_not_summable': list or tuple
A pair of numbers `[a, b]`, where `a > 0` and `b >= 0`, for the rule :math:`h_k = a / (b + k)` for each `k`.
* 'geometric_series': list or tuple
A pair of numbers `[a, r]`, where `a > 0` and `0 < r < 1`, for the rule :math:`h_k = a * r^k` for each `k`.
* 'user_defined': Callable
A function that takes the current iteration `k` as input and returns the step size (float).
Examples:
>>> from specular.optimization.step_size import StepSize
>>>
>>> # 'constant': h_k = a
>>> step = StepSize(name='constant', parameters=0.5)
>>>
>>> # 'not_summable' rule: h_k = a / sqrt(k)
>>> # a = 2.0
>>> step = StepSize(name='not_summable', parameters=2.0)
>>>
>>> # 'square_summable_not_summable' rule: h_k = a / (b + k
>>> # a = 10, b = 2
>>> step = StepSize(name='square_summable_not_summable', parameters=[10.0, 2.0])
>>>
>>> # 'geometric_series' rule: h_k = a * r^k
>>> # a = 1.0, r = 0.5
>>> step = StepSize(name='geometric_series', parameters=[1.0, 0.5])
>>>
>>> # 'user_defined' callable.
>>> # Custom rule: h_k = 1 / k^2
>>> custom_rule = lambda k: 1.0 / (k**2)
>>> step = StepSize(name='user_defined', parameters=custom_rule)
"""
self.step_size = name
self.parameters = parameters
init_methods = {
'constant': self._init_constant,
'not_summable': self._init_not_summable,
'square_summable_not_summable': self._init_square_summable,
'geometric_series': self._init_geometric,
'user_defined': self._init_user_defined
}
if name not in init_methods:
raise ValueError(f"Invalid step size '{name}'. Options: {self.__options__}")
init_methods[name]()
def __call__(self, k: int) -> float:
"""
Returns the step size at iteration k.
"""
return self._rule(k)
# ==== Initialization Methods ====
def _init_constant(self):
if not isinstance(self.parameters, (float, int, np.floating)):
raise TypeError(f"Invalid type: number required. Got {type(self.parameters)}")
if self.parameters <= 0:
raise ValueError(f"Invalid value: positive number required. Got {self.parameters}")
self.a = float(self.parameters)
self._rule = self._calc_constant
def _init_not_summable(self):
if not isinstance(self.parameters, (float, int, np.floating)):
raise TypeError(f"Invalid type: number required. Got {type(self.parameters)}")
if self.parameters <= 0:
raise ValueError(f"Invalid value: positive number required. Got {self.parameters}")
self.a = float(self.parameters)
self._rule = self._calc_not_summable
def _init_square_summable(self):
if not isinstance(self.parameters, (tuple, list, np.ndarray)):
raise TypeError(f"Invalid type: list/tuple required. Got {type(self.parameters)}")
if len(self.parameters) != 2:
raise ValueError(f"Invalid length: 2 parameters [a, b] required. Got {len(self.parameters)}")
self.a, self.b = self.parameters[0], self.parameters[1]
if self.a <= 0 or self.b < 0:
raise ValueError(f"Invalid parameters: a > 0 and b >= 0 required. Got a={self.a}, b={self.b}")
self._rule = self._calc_square_summable_not_summable
def _init_geometric(self):
if not isinstance(self.parameters, (tuple, list, np.ndarray)):
raise TypeError(f"Invalid type: list/tuple required. Got {type(self.parameters)}")
if len(self.parameters) != 2:
raise ValueError(f"Invalid length: 2 parameters [a, r] required. Got {len(self.parameters)}")
self.a, self.r = self.parameters[0], self.parameters[1]
if self.a <= 0 or not (0.0 < self.r < 1.0):
raise ValueError(f"Invalid parameters: a > 0 and 0 < r < 1 required. Got a={self.a}, r={self.r}")
self._rule = self._calc_geometric_series
def _init_user_defined(self):
if not callable(self.parameters):
raise TypeError("Invalid type: callable function required.")
self._rule = self.parameters
# ==== Calculation Methods ====
def _calc_constant(self, k: int) -> float:
"""
h_k = a
"""
return self.a
def _calc_not_summable(self, k: int) -> float:
"""
h_k = a / sqrt{k}
"""
return self.a / math.sqrt(k)
def _calc_square_summable_not_summable(self, k: int) -> float:
"""
h_k = a / (b + k)
"""
return self.a / (self.b + k)
def _calc_geometric_series(self, k: int) -> float:
"""
h_k = a * r**k
"""
return self.a * (self.r ** k)
|
__call__(k)
Returns the step size at iteration k.
Source code in specular\optimization\step_size.py
| def __call__(self, k: int) -> float:
"""
Returns the step size at iteration k.
"""
return self._rule(k)
|
__init__(name, parameters)
The step size rules for optimization methods \(x_{k+1} = x_k - h_k s_k\), where \(s_k\) is the search direction and \(h_k > 0\) is the step size at iteration \(k >= 1\).
Parameters:
| Name |
Type |
Description |
Default |
name
|
str
|
Options: 'constant', 'not_summable', 'square_summable_not_summable', 'geometric_series', 'user_defined'
|
required
|
parameters
|
float | int | tuple | list | ndarray | Callable
|
The parameters required for the selected step size rule:
-
'constant': float or int
A number a > 0 for the rule :math:h_k = a for each k.
-
'not_summable': float or int
A number a > 0 for the rule :math:h_k = a / sqrt{k} for each k.
-
'square_summable_not_summable': list or tuple
A pair of numbers [a, b], where a > 0 and b >= 0, for the rule :math:h_k = a / (b + k) for each k.
-
'geometric_series': list or tuple
A pair of numbers [a, r], where a > 0 and 0 < r < 1, for the rule :math:h_k = a * r^k for each k.
-
'user_defined': Callable
A function that takes the current iteration k as input and returns the step size (float).
|
required
|
Examples:
>>> from specular.optimization.step_size import StepSize
>>>
>>> # 'constant': h_k = a
>>> step = StepSize(name='constant', parameters=0.5)
>>>
>>> # 'not_summable' rule: h_k = a / sqrt(k)
>>> # a = 2.0
>>> step = StepSize(name='not_summable', parameters=2.0)
>>>
>>> # 'square_summable_not_summable' rule: h_k = a / (b + k
>>> # a = 10, b = 2
>>> step = StepSize(name='square_summable_not_summable', parameters=[10.0, 2.0])
>>>
>>> # 'geometric_series' rule: h_k = a * r^k
>>> # a = 1.0, r = 0.5
>>> step = StepSize(name='geometric_series', parameters=[1.0, 0.5])
>>>
>>> # 'user_defined' callable.
>>> # Custom rule: h_k = 1 / k^2
>>> custom_rule = lambda k: 1.0 / (k**2)
>>> step = StepSize(name='user_defined', parameters=custom_rule)
Source code in specular\optimization\step_size.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88 | def __init__(
self,
name: str,
parameters: float | np.floating | int | Tuple | list | np.ndarray | Callable
):
"""
The step size rules for optimization methods $x_{k+1} = x_k - h_k s_k$, where $s_k$ is the search direction and $h_k > 0$ is the step size at iteration $k >= 1$.
Parameters:
name (str):
Options: 'constant', 'not_summable', 'square_summable_not_summable', 'geometric_series', 'user_defined'
parameters (float | int | tuple | list | np.ndarray | Callable):
The parameters required for the selected step size rule:
* 'constant': float or int
A number `a > 0` for the rule :math:`h_k = a` for each `k`.
* 'not_summable': float or int
A number `a > 0` for the rule :math:`h_k = a / sqrt{k}` for each `k`.
* 'square_summable_not_summable': list or tuple
A pair of numbers `[a, b]`, where `a > 0` and `b >= 0`, for the rule :math:`h_k = a / (b + k)` for each `k`.
* 'geometric_series': list or tuple
A pair of numbers `[a, r]`, where `a > 0` and `0 < r < 1`, for the rule :math:`h_k = a * r^k` for each `k`.
* 'user_defined': Callable
A function that takes the current iteration `k` as input and returns the step size (float).
Examples:
>>> from specular.optimization.step_size import StepSize
>>>
>>> # 'constant': h_k = a
>>> step = StepSize(name='constant', parameters=0.5)
>>>
>>> # 'not_summable' rule: h_k = a / sqrt(k)
>>> # a = 2.0
>>> step = StepSize(name='not_summable', parameters=2.0)
>>>
>>> # 'square_summable_not_summable' rule: h_k = a / (b + k
>>> # a = 10, b = 2
>>> step = StepSize(name='square_summable_not_summable', parameters=[10.0, 2.0])
>>>
>>> # 'geometric_series' rule: h_k = a * r^k
>>> # a = 1.0, r = 0.5
>>> step = StepSize(name='geometric_series', parameters=[1.0, 0.5])
>>>
>>> # 'user_defined' callable.
>>> # Custom rule: h_k = 1 / k^2
>>> custom_rule = lambda k: 1.0 / (k**2)
>>> step = StepSize(name='user_defined', parameters=custom_rule)
"""
self.step_size = name
self.parameters = parameters
init_methods = {
'constant': self._init_constant,
'not_summable': self._init_not_summable,
'square_summable_not_summable': self._init_square_summable,
'geometric_series': self._init_geometric,
'user_defined': self._init_user_defined
}
if name not in init_methods:
raise ValueError(f"Invalid step size '{name}'. Options: {self.__options__}")
init_methods[name]()
|
specular.optimization.solver
gradient_method(f, x_0, step_size, h=1e-06, form='specular gradient', tol=1e-06, zero_tol=1e-08, max_iter=1000, f_j=None, m=1, switch_iter=2, record_history=True, print_bar=True)
Minimize a nonsmooth convex function using the current backend.
Source code in specular\optimization\solver.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60 | def gradient_method(
f: Callable[
[int | float | np.number | list | np.ndarray],
int | float | np.number,
],
x_0: Any,
step_size: StepSize,
h: float = 1e-6,
form: str = "specular gradient",
tol: float = 1e-6,
zero_tol: float = 1e-8,
max_iter: int = 1000,
f_j: Sequence[ComponentFunc] | Callable | None = None,
m: int = 1,
switch_iter: int | None = 2,
record_history: bool = True,
print_bar: bool = True,
) -> OptimizationResult:
"""
Minimize a nonsmooth convex function using the current backend.
"""
impl = cast(Any, _get_solver_module().gradient_method)
return impl(
f=f,
x_0=x_0,
step_size=step_size,
h=h,
form=form,
tol=tol,
zero_tol=zero_tol,
max_iter=max_iter,
f_j=f_j,
m=m,
switch_iter=switch_iter,
record_history=record_history,
print_bar=print_bar,
)
|
specular.optimization.result
OptimizationResult
Source code in specular\optimization\result.py
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51 | class OptimizationResult:
def __init__(
self,
method: str,
solution: np.ndarray,
func_val: int | float | np.number,
iteration: int,
runtime: float,
all_history: dict
):
self.method = method
self.solution = solution
self.func_val = func_val
self.iteration = iteration
self.runtime = runtime
self.all_history = all_history
def __repr__(self):
return (
f"[{self.method}]\n"
f" solution: {self.solution}\n"
f" func value: {self.func_val}\n"
f" iteration: {self.iteration}"
)
def last_record(
self
) -> Tuple[float, float, float]:
"""
Returns the final solution x, the value of f at x, and the runtime as a tuple.
Returns:
(x, f(x), runtime)
"""
return self.solution, self.func_val, self.runtime # type: ignore
def history(
self
) -> Tuple[np.ndarray, np.ndarray, float]:
"""
Returns the time grid and the numerical solution as a tuple.
Returns:
(x_history, f_history, runtime)
"""
return self.all_history["variables"], self.all_history["values"], self.runtime
|
history()
Returns the time grid and the numerical solution as a tuple.
Returns:
| Type |
Description |
Tuple[ndarray, ndarray, float]
|
(x_history, f_history, runtime)
|
Source code in specular\optimization\result.py
42
43
44
45
46
47
48
49
50
51 | def history(
self
) -> Tuple[np.ndarray, np.ndarray, float]:
"""
Returns the time grid and the numerical solution as a tuple.
Returns:
(x_history, f_history, runtime)
"""
return self.all_history["variables"], self.all_history["values"], self.runtime
|
last_record()
Returns the final solution x, the value of f at x, and the runtime as a tuple.
Returns:
| Type |
Description |
Tuple[float, float, float]
|
|
Source code in specular\optimization\result.py
31
32
33
34
35
36
37
38
39
40 | def last_record(
self
) -> Tuple[float, float, float]:
"""
Returns the final solution x, the value of f at x, and the runtime as a tuple.
Returns:
(x, f(x), runtime)
"""
return self.solution, self.func_val, self.runtime # type: ignore
|