Skip to content

Commit 859330a

Browse files
committed
getting hessian to work for simple formulation of MLE
1 parent 1062ffc commit 859330a

File tree

7 files changed

+432
-23
lines changed

7 files changed

+432
-23
lines changed

cvxpy/atoms/affine/binary_operators.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import numpy as np
2222
import scipy.sparse as sp
2323

24+
from cvxpy.expressions.variable import Variable
2425
import cvxpy.lin_ops.lin_op as lo
2526
import cvxpy.lin_ops.lin_utils as lu
2627
import cvxpy.utilities as u
@@ -206,6 +207,32 @@ def _grad(self, values):
206207
DY = sp.kron(sp.eye(p, format='csc'), X, format='csc').T
207208

208209
return [DX, DY]
210+
211+
def _hess(self, values):
212+
"""Compute the Hessian of elementwise multiplication w.r.t. each argument.
213+
214+
For z = x * y (elementwise), returns:
215+
- d2z/dx2 = diag(y)
216+
- d2z/dy2 = diag(x)
217+
218+
Args:
219+
values: A list of numeric values for the arguments [x, y].
220+
221+
Returns:
222+
A list of SciPy CSC sparse matrices [D2X, D2Y].
223+
"""
224+
if isinstance(self.args[0], Variable) or isinstance(self.args[1], Variable):
225+
return {(self.args[0], self.args[1]): 1.0,
226+
(self.args[1], self.args[0]): 1.0}
227+
x = values[0]
228+
y = values[1]
229+
# what is the hessian of elementwise multiplication?
230+
# Flatten in case inputs are not 1D
231+
x = np.asarray(x).flatten(order='F')
232+
y = np.asarray(y).flatten(order='F')
233+
D2X = sp.diags(y, format='csc')
234+
D2Y = sp.diags(x, format='csc')
235+
return [D2X, D2Y]
209236

210237
def graph_implementation(
211238
self, arg_objs, shape: Tuple[int, ...], data=None
@@ -319,6 +346,29 @@ def _grad(self, values):
319346
DY = sp.diags(x, format='csc')
320347
return [DX, DY]
321348

349+
def _hess(self, values):
350+
"""Compute the Hessian of elementwise multiplication w.r.t. each argument.
351+
352+
For z = x * y (elementwise), returns:
353+
- d2z/dx2 = diag(y)
354+
- d2z/dy2 = diag(x)
355+
356+
Args:
357+
values: A list of numeric values for the arguments [x, y].
358+
359+
Returns:
360+
A list of SciPy CSC sparse matrices [D2X, D2Y].
361+
"""
362+
x = values[0]
363+
y = values[1]
364+
# what is the hessian of elementwise multiplication?
365+
# Flatten in case inputs are not 1D
366+
x = np.asarray(x).flatten(order='F')
367+
y = np.asarray(y).flatten(order='F')
368+
D2X = sp.diags(y, format='csc')
369+
D2Y = sp.diags(x, format='csc')
370+
return [D2X, D2Y]
371+
322372
def graph_implementation(
323373
self, arg_objs, shape: Tuple[int, ...], data=None
324374
) -> Tuple[lo.LinOp, List[Constraint]]:

cvxpy/atoms/atom.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -442,12 +442,12 @@ def grad(self):
442442
@property
443443
def hess(self):
444444
from cvxpy.atoms.elementwise.log import log
445-
446-
print("INSIDE HESS IN ATOM")
445+
from cvxpy.atoms.elementwise.power import power
446+
# print("INSIDE HESS IN ATOM")
447447

448448
# Short-circuit to all zeros if known to be constant.
449-
#if self.is_constant():
450-
# return u.grad.constant_grad(self)
449+
if self.is_constant():
450+
return u.grad.constant_grad(self)
451451

452452
# Returns None if variable values not supplied.
453453
arg_values = []
@@ -459,6 +459,9 @@ def hess(self):
459459

460460
# A list of gradients w.r.t. arguments
461461
hess_self = self._hess(arg_values)
462+
if isinstance(hess_self, dict):
463+
# print("hess_self is a dict")
464+
return hess_self
462465
# The Chain rule.
463466
result = {}
464467
for idx, arg in enumerate(self.args):
@@ -467,7 +470,7 @@ def hess(self):
467470
hess_arg = arg.hess
468471

469472
for key in hess_arg:
470-
if isinstance(self, log):
473+
if isinstance(self, (log, power)):
471474
hess_arg[key] = 1.0
472475

473476
# None indicates gradient is not defined.

cvxpy/atoms/elementwise/power.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,35 @@ def _grad(self, values):
380380
grad_vals = float(p)*np.power(values[0], float(p)-1)
381381
return [power.elemwise_grad_to_diag(grad_vals, rows, cols)]
382382

383+
def _hess(self, values):
384+
"""TODO: write message
385+
"""
386+
rows = self.args[0].size
387+
cols = self.size
388+
389+
if self.p_rational is not None:
390+
p = self.p_rational
391+
elif self.p.value is not None:
392+
p = self.p.value
393+
else:
394+
raise ValueError("Cannot compute grad of parametrized power when "
395+
"parameter value is unspecified.")
396+
397+
if p == 0:
398+
# All zeros.
399+
return [sp.csc_array((rows, cols), dtype='float64')]
400+
# Outside domain or on boundary.
401+
if not is_power2(p) and np.min(values[0]) <= 0:
402+
if p < 1:
403+
# Non-differentiable.
404+
return [None]
405+
else:
406+
# Round up to zero.
407+
values[0] = np.maximum(values[0], 0)
408+
409+
hess_vals = float(p)*float(p-1)*np.power(values[0], float(p)-2)
410+
return [power.elemwise_grad_to_diag(hess_vals, rows, cols)]
411+
383412
def _domain(self) -> List[Constraint]:
384413
"""Returns constraints describing the domain of the node.
385414
"""

cvxpy/expressions/constants/constant.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,13 @@ def grad(self):
116116
"""
117117
return {}
118118

119+
@property
120+
def hess(self):
121+
"""
122+
The hessian of a constant is also an empty dictionary.
123+
"""
124+
return {}
125+
119126
@property
120127
def shape(self) -> Tuple[int, ...]:
121128
"""Returns the (row, col) dimensions of the expression.

cvxpy/reductions/solvers/nlp_solvers/ipopt_nlpif.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -364,11 +364,11 @@ def hessian(self, x, duals, obj_factor):
364364
# This is done by looping through each constraint and each
365365
# pair of variables and summing up the hessian contributions.
366366
constr_offset = 0
367-
print("checkpoint 1")
367+
#print("checkpoint 1")
368368
for constraint in self.problem.constraints:
369-
print("checkpoint 2")
369+
#print("checkpoint 2")
370370
hess_dict = constraint.expr.hess
371-
print("checkpoint 3")
371+
#print("checkpoint 3")
372372
# we have to make sure that the dual variables correspond
373373
# to the constraints in the same order
374374
constr_dual = duals[constr_offset:constr_offset + constraint.size]
@@ -380,18 +380,17 @@ def hessian(self, x, duals, obj_factor):
380380
var_hess = hess_dict[(var1, var2)]
381381
if sp.issparse(var_hess):
382382
var_hess = var_hess.toarray()
383-
384383
r1, r2 = var1.size, var2.size
385384
# insert the block in the correct location
386385
hess_lagrangian[row_offset:row_offset+r1,
387386
col_offset:col_offset+r2] += constr_dual * var_hess
388387
col_offset += var2.size
389388
row_offset += var1.size
390389
constr_offset += constraint.size
391-
392-
#print("hess: ", hess)
393-
return hess_lagrangian
394-
390+
# return lower triangular part of the hessian
391+
row, col = self.hessianstructure()
392+
hess_lagrangian = hess_lagrangian[row, col]
393+
return hess_lagrangian
395394

396395
class Bounds():
397396
def __init__(self, problem):

cvxpy/tests/NLP_tests/remove_later.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@
1818
for method in METHODS:
1919
print("Method, n, scale factor: ", method, n, factor)
2020
if method == 1:
21-
sigma = cp.Variable((1, ))
21+
sigma = cp.Variable((1,))
2222
obj = (n / 2) * cp.log(2*np.pi*cp.square(sigma)) + (1 / (2 * cp.square(sigma))) * res
2323
constraints = []
24+
"""
2425
elif method == 2:
2526
sigma2 = cp.Variable((1, ))
2627
obj = (n / 2) * cp.log( 2 * np.pi * sigma2) + (1 / (2 * sigma2)) * res
@@ -39,16 +40,12 @@
3940
sigma = cp.Variable((1, ))
4041
obj = n * cp.log(np.sqrt(2*np.pi)*sigma * -1 * -1 * 2 * 0.5) + (1 / (2 * cp.square(sigma))) * res
4142
constraints = []
42-
43+
"""
4344
problem = cp.Problem(cp.Minimize(obj), constraints)
4445
problem.solve(solver=cp.IPOPT, nlp=True)
45-
46-
47-
reduction = Expr2Smooth(problem)
48-
new_prob, inv_data = reduction.apply(problem)
49-
print(str(new_prob))
50-
51-
46+
#reduction = Expr2Smooth(problem)
47+
#new_prob, inv_data = reduction.apply(problem)
48+
#print(str(new_prob))
5249
print("sigma.value: ", sigma.value)
5350
print("sigma_opt: ", sigma_opt)
54-
assert(np.abs(sigma.value - sigma_opt) / np.max([1, np.abs(sigma_opt)]) <= TOL)
51+
assert(np.abs(sigma.value - sigma_opt) / np.max([1, np.abs(sigma_opt)]) <= TOL)

0 commit comments

Comments
 (0)