Skip to content

Commit 6c54fe5

Browse files
authored
Merge branch 'master' into bounds_investigation
2 parents 8f24df7 + 115bf77 commit 6c54fe5

File tree

8 files changed

+121
-49
lines changed

8 files changed

+121
-49
lines changed

cvxpy/atoms/affine/affine_atom.py

Lines changed: 33 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -109,58 +109,54 @@ def is_nsd(self) -> bool:
109109
return True
110110

111111
def _grad(self, values) -> List[Any]:
112-
"""Gives the (sub/super)gradient of the atom w.r.t. each argument.
112+
"""Computes the gradient of the affine atom w.r.t. each argument.
113113
114-
Matrix expressions are vectorized, so the gradient is a matrix.
114+
For affine atoms, the gradient is constant and independent of argument values.
115+
We compute it by constructing the canonical matrix representation and extracting
116+
the linear coefficients.
115117
116118
Args:
117-
values: A list of numeric values for the arguments.
119+
values: Argument values (unused for affine atoms).
118120
119121
Returns:
120-
A list of SciPy CSC sparse matrices or None.
122+
List of gradient matrices, one for each argument.
121123
"""
122-
# TODO should be a simple function in cvxcore for this.
123-
# Make a fake lin op tree for the function.
124+
# Create fake variables for each non-constant argument to build the linear system
124125
fake_args = []
125126
var_offsets = {}
126-
offset = 0
127+
var_length = 0
128+
127129
for idx, arg in enumerate(self.args):
128130
if arg.is_constant():
129-
fake_args += [Constant(arg.value).canonical_form[0]]
131+
fake_args.append(Constant(arg.value).canonical_form[0])
130132
else:
131-
fake_args += [lu.create_var(arg.shape, idx)]
132-
var_offsets[idx] = offset
133-
offset += arg.size
134-
var_length = offset
135-
fake_expr, _ = self.graph_implementation(fake_args, self.shape,
136-
self.get_data())
137-
param_to_size = {lo.CONSTANT_ID: 1}
138-
param_to_col = {lo.CONSTANT_ID: 0}
139-
# Get the matrix representation of the function.
133+
fake_args.append(lu.create_var(arg.shape, idx))
134+
var_offsets[idx] = var_length
135+
var_length += arg.size
136+
137+
# Get the canonical matrix representation: f(x) = Ax + b
138+
fake_expr, _ = self.graph_implementation(fake_args, self.shape, self.get_data())
140139
canon_mat = canonInterface.get_problem_matrix(
141-
[fake_expr],
142-
var_length,
143-
var_offsets,
144-
param_to_size,
145-
param_to_col,
146-
self.size,
140+
[fake_expr], var_length, var_offsets,
141+
{lo.CONSTANT_ID: 1}, {lo.CONSTANT_ID: 0}, self.size
147142
)
148-
# HACK TODO TODO convert tensors back to vectors.
149-
# COO = (V[lo.CONSTANT_ID][0], (J[lo.CONSTANT_ID][0], I[lo.CONSTANT_ID][0]))
150-
shape = (var_length + 1, self.size)
151-
stacked_grad = canon_mat.reshape(shape).tocsc()[:-1, :]
152-
# Break up into per argument matrices.
143+
144+
# Extract gradient matrix A (exclude constant offset b)
145+
grad_matrix = canon_mat.reshape((var_length + 1, self.size)).tocsc()[:-1, :]
146+
147+
# Split gradients by argument
153148
grad_list = []
154-
start = 0
149+
var_start = 0
155150
for arg in self.args:
156151
if arg.is_constant():
157-
grad_shape = (arg.size, shape[1])
158-
if grad_shape == (1, 1):
159-
grad_list += [0]
160-
else:
161-
grad_list += [sp.coo_matrix(grad_shape, dtype='float64')]
152+
# Zero gradient for constants
153+
grad_shape = (arg.size, self.size)
154+
grad_list.append(0 if grad_shape == (1, 1) else
155+
sp.coo_matrix(grad_shape, dtype='float64'))
162156
else:
163-
stop = start + arg.size
164-
grad_list += [stacked_grad[start:stop, :]]
165-
start = stop
157+
# Extract gradient block for this variable
158+
var_end = var_start + arg.size
159+
grad_list.append(grad_matrix[var_start:var_end, :])
160+
var_start = var_end
161+
166162
return grad_list

cvxpy/atoms/atom.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,16 @@ def grad(self):
439439

440440
return result
441441

442+
def hess(self, duals):
443+
"""
444+
Compute the hessian-vector product of a scalar function with dual
445+
values.
446+
Here the dual values should correspond to the dual values of the
447+
constraint function or None if we are taking the hessian of the objective.
448+
We must first slice the duals array to get the relevant values.
449+
"""
450+
pass
451+
442452
@abc.abstractmethod
443453
def _grad(self, values):
444454
"""Gives the (sub/super)gradient of the atom w.r.t. each argument.

cvxpy/reductions/expr2smooth/canonicalizers/log_canon.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
limitations under the License.
1515
"""
1616

17+
import numpy as np
18+
19+
from cvxpy.expressions.constants import Constant
1720
from cvxpy.expressions.variable import Variable
1821
from cvxpy.expressions.constants import Constant
1922
import numpy as np
@@ -34,6 +37,21 @@ def collect_constant_and_variable(expr, constants, variable):
3437
# Perhaps this should be a parameter exposed to the user?
3538
LOWER_BOUND = 1e-5
3639

40+
def collect_constant_and_variable(expr, constants, variable):
41+
if isinstance(expr, Constant):
42+
constants.append(expr)
43+
elif isinstance(expr, Variable):
44+
variable.append(expr)
45+
elif hasattr(expr, "args"):
46+
for subexpr in expr.args:
47+
collect_constant_and_variable(subexpr, constants, variable)
48+
49+
assert(len(variable) <= 1)
50+
51+
# DCED: Without this lower bound the stress test for ML Gaussian non-zero mean fails.
52+
# Perhaps this should be a parameter exposed to the user?
53+
LOWER_BOUND = 1e-5
54+
3755
def log_canon(expr, args):
3856
t = Variable(args[0].size, bounds=[LOWER_BOUND, None])
3957

cvxpy/reductions/expr2smooth/canonicalizers/power_canon.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,13 @@ def power_canon(expr, args):
4242
#return t, [t**(1/p) == x, t >= 0]
4343
elif p > 1:
4444
t = Variable(args[0].shape)
45-
if args[0].value is not None:
45+
46+
# DCED: for Gaussian ML it works better if we include the second
47+
# condition here
48+
if args[0].value is not None and np.all(args[0].value >= 1):
4649
t.value = args[0].value
4750
else:
4851
t.value = expr.point_in_domain()
49-
#t.value = np.zeros(shape)
5052

5153
return expr.copy([t]), [t==args[0]]
5254
else:

cvxpy/reductions/solvers/nlp_solvers/ipopt_nlpif.py

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,6 @@ def __init__(self, problem, inital_point):
213213

214214
def objective(self, x):
215215
"""Returns the scalar value of the objective given x."""
216-
# Set the variable value
217216
offset = 0
218217
for var in self.main_var:
219218
size = var.size
@@ -227,7 +226,6 @@ def gradient(self, x):
227226
#import pdb
228227
#pdb.set_trace()
229228
"""Returns the gradient of the objective with respect to x."""
230-
# compute the gradient using _grad
231229
offset = 0
232230
for var in self.main_var:
233231
size = var.size
@@ -248,15 +246,13 @@ def gradient(self, x):
248246

249247
def constraints(self, x):
250248
"""Returns the constraint values."""
251-
# Set the variable value
252249
offset = 0
253250
#import pdb
254251
#pdb.set_trace()
255252
for var in self.main_var:
256253
size = var.size
257254
var.value = x[offset:offset+size].reshape(var.shape, order='F')
258255
offset += size
259-
260256
# Evaluate all constraints
261257
constraint_values = []
262258
for constraint in self.problem.constraints:
@@ -265,15 +261,12 @@ def constraints(self, x):
265261

266262
def jacobian(self, x):
267263
"""Returns only the non-zero values of the Jacobian."""
268-
#import pdb
269-
#pdb.set_trace()
270264
# Set variable values
271265
offset = 0
272266
for var in self.main_var:
273267
size = var.size
274268
var.value = x[offset:offset+size].reshape(var.shape, order='F')
275269
offset += size
276-
277270
values = []
278271
for constraint in self.problem.constraints:
279272
# get the jacobian of the constraint
@@ -329,9 +322,44 @@ def jacobianstructure(self):
329322
col_offset += var.size
330323
row_offset += constraint.size
331324
self.jacobian_idxs[constraint] = constraint_jac
332-
333325
return (np.array(rows), np.array(cols))
334326

327+
def hessian(self, x, duals, obj_factor):
328+
offset = 0
329+
for var in self.main_var:
330+
size = var.size
331+
var.value = x[offset:offset+size].reshape(var.shape, order='F')
332+
offset += size
333+
hess = np.zeros((x.size, x.size), dtype=np.float64)
334+
# hess_dict = self.problem.objective.expr.hess(obj_factor)
335+
# if we specify the problem in graph form (i.e. t=obj),
336+
# the objective hessian will always be zero.
337+
# To compute the hessian of the constraints, we need to gather the
338+
# second derivatives from each constraint.
339+
# This is done by looping through each constraint and each
340+
# pair of variables and summing up the hessian contributions.
341+
constr_offset = 0
342+
for constraint in self.problem.constraints:
343+
constr_hess = constraint.expr.hess()
344+
# we have to make sure that the dual variables correspond
345+
# to the constraints in the same order
346+
constr_dual = duals[constr_offset:constr_offset + constraint.size]
347+
row_offset = 0
348+
for var1 in self.main_var:
349+
col_offset = 0
350+
for var2 in self.main_var:
351+
hess[var1.index * row_offset, var2.index * col_offset] += (
352+
constr_dual * constr_hess.get((var1, var2), 0).toarray()
353+
)
354+
col_offset += var2.size
355+
row_offset += var1.size
356+
constr_offset += constraint.size
357+
return hess
358+
359+
def hessianstructure(self):
360+
pass
361+
362+
335363
class Bounds():
336364
def __init__(self, problem):
337365
self.problem = problem

cvxpy/sandbox/nmf_fro.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import numpy as np
2+
3+
import cvxpy as cp
4+
5+
np.random.seed(1)
6+
7+
n, m, k = 5, 4, 2
8+
noise_level = 0.05
9+
X_true = np.random.rand(n, k)
10+
Y_true = np.random.rand(k, m)
11+
A_noisy = X_true @ Y_true + noise_level * np.random.randn(n, m)
12+
A_noise = np.clip(A_noisy, 0, None)
13+
# initialize X and Y to random nonnegative values
14+
X = cp.Variable((n, k), bounds=[0, np.inf])
15+
X.value = np.ones((n, k))
16+
Y = cp.Variable((k, m), bounds=[0, np.inf])
17+
Y.value = np.ones((k, m))
18+
obj = cp.sum(cp.square(A_noise - X @ Y))
19+
prob = cp.Problem(cp.Minimize(obj))
20+
prob.solve(solver=cp.IPOPT, nlp=True, verbose=True)

cvxpy/sandbox/tests_problem_references/NMF_fro.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,3 @@
1919
obj = cp.sum(cp.square(A_noisy - X @ Y))
2020
problem = cp.Problem(cp.Minimize(obj))
2121
problem.solve(solver=cp.IPOPT, nlp=True, verbose=True)
22-

cvxpy/tests/NLP_tests/test_ML_Gaussian_stress.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
# TODO (DCED): should try eg. student-t regression
66

7-
87
class TestStressMLE():
98

109
def test_zero_mean(self):

0 commit comments

Comments
 (0)