Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 130 additions & 2 deletions odl/solvers/functional/default_functionals.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
proximal_l1, proximal_convex_conj_l1, proximal_l2, proximal_convex_conj_l2,
proximal_l2_squared, proximal_const_func, proximal_box_constraint,
proximal_convex_conj, proximal_convex_conj_kl,
proximal_convex_conj_kl_cross_entropy,
proximal_convex_conj_kl_cross_entropy, proximal_huber_norm,
combine_proximals)
from odl.util import conj_exponent

Expand All @@ -35,7 +35,7 @@
'QuadraticForm',
'NuclearNorm', 'IndicatorNuclearNormUnitBall',
'ScalingFunctional', 'IdentityFunctional',
'MoreauEnvelope')
'MoreauEnvelope', 'HuberNorm')


class LpNorm(Functional):
Expand Down Expand Up @@ -2292,6 +2292,134 @@ def gradient(self):
(1 / self.sigma) * self.functional.proximal(self.sigma))


class HuberNorm(Functional):

"""The Huber norm functional.

Notes
-----
The functional :math:`F` with smoothing :math:`\\epsilon>0` is given by

.. math::
F(x)
=
\\begin{cases}
\\frac{1}{2 \\epsilon} x^2 & \\text{if } |x| \leq \\epsilon
\\\\
|x| - \\frac{\\epsilon}{2} & \\text{if } |x| > \\epsilon,
\\end{cases}

and the Huber norm is the integral of this functional over the domain,
i.e., the Huber norm is given by

.. math::
\\int_\Omega F(x) dx.

In the discrete case, this becomes

.. math::
\\sum_{i=1}^n F(x_i).

"""

def __init__(self, space, epsilon):
"""Initialize a new instance.

Parameters
----------
space : `DiscreteLp` or `FnBase`
Domain of the functional.
epsilon : float
The parameter of the Huber norm functional.

Examples
--------
Example of initializing the Huber norm functional

>>> space = odl.uniform_discr(0, 1, 14)
>>> epsilon = 0.1
>>> huber_norm = odl.solvers.HuberNorm(space, epsilon)

Check that if all elements are > epsilon we get the L1-norm modified
epsilon/2 * the mass of the space
>>> l1_norm = odl.solvers.L1Norm(space)
>>> element = space.one()
>>> constant = epsilon/2 * element.inner(element)
>>> (huber_norm(element) - (l1_norm(element) - constant)) < 1e-5
True

Check that if all elements are < epsilon we get the L2-norm modified
with weight 1/(2*epsilon)
>>> l2_norm = odl.solvers.L2Norm(space)
>>> element = (epsilon/2) * space.one()
>>> (huber_norm(element) - 1/(2*epsilon) * l2_norm(element)) < 1e-5
True
"""
self.__epsilon = float(epsilon)
super(HuberNorm, self).__init__(space=space, linear=False,
grad_lipschitz=2)

@property
def epsilon(self):
"""The smoothing parameter of the Huber norm functional."""
return self.__epsilon

def _call(self, x):
"""Return ``self(x)``."""
indices = x.ufuncs.absolute().asarray() < self.epsilon
indices = np.float32(indices)

tmp = ((x * indices)**2 / (2.0 * self.epsilon) +
(x.ufuncs.absolute() - self.epsilon / 2.0) * (1-indices))

return tmp.inner(self.domain.one())

@property
def gradient(self):
"""Gradient operator of the functional."""
functional = self

class HuberNormGradient(Operator):

"""The gradient operator of this functional."""

def __init__(self):
"""Initialize a new instance."""
super(HuberNormGradient, self).__init__(functional.domain,
functional.domain,
linear=False)

# TODO: Update this call. Might not work for PorductSpaces
def _call(self, x):
"""Apply the gradient operator to the given point."""
indices = x.ufuncs.absolute().asarray() < functional.epsilon
indices = np.float32(indices)

to_return = ((x * indices) / (functional.epsilon) +
(x).ufuncs.sign() * (1-indices))

return to_return

return HuberNormGradient()

@property
def proximal(self):
"""Return the ``proximal factory`` of the functional.

See Also
--------
odl.solvers.proximal_huber_norm : `proximal factory` for the Huber
norm.
"""
return proximal_huber_norm(space=self.domain, epsilon=self.epsilon)

def __repr__(self):
"""Return ``repr(self)``."""
return '{}({!r}, {})'.format(self.__class__.__name__,
self.domain,
self.epsilon)


if __name__ == '__main__':
# pylint: disable=wrong-import-position
from odl.util.testutils import run_doctests
Expand Down
64 changes: 63 additions & 1 deletion odl/solvers/nonsmooth/proximal_operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
'proximal_l1', 'proximal_convex_conj_l1',
'proximal_l2', 'proximal_convex_conj_l2',
'proximal_l2_squared', 'proximal_convex_conj_l2_squared',
'proximal_convex_conj_kl', 'proximal_convex_conj_kl_cross_entropy')
'proximal_convex_conj_kl', 'proximal_convex_conj_kl_cross_entropy',
'proximal_huber_norm')


def combine_proximals(*factory_list):
Expand Down Expand Up @@ -1406,6 +1407,67 @@ def _call(self, x, out):
return ProximalConvexConjKLCrossEntropy


def proximal_huber_norm(space, epsilon):
"""Proximal factory of the Huber norm.

Parameters
----------
space : `FnBase`
Space X which is the domain of the functional F
epsilon : float
The parameter of the Huber norm functional.

Returns
-------
prox_factory : function
Factory for the proximal operator to be initialized.

See Also
--------
odl.solvers.HuberNorm : the Huber norm functional

Notes
-----
The proximal operator is given by given by the proximal operator of
1/(2 * epsilon) * L2 norm in points that are <= epsilon, and by the
proximal operator of the l1 norm in points that are > epsilon.
"""

epsilon = float(epsilon)

class ProximalHuberNorm(Operator):

"""Proximal operator of conjugate of cross entropy KL divergence."""

def __init__(self, sigma):
"""Initialize a new instance.

Parameters
----------
sigma : positive float
"""
self.sigma = float(sigma)
super(ProximalHuberNorm, self).__init__(
domain=space, range=space, linear=False)

def _call(self, x, out):
"""Apply the operator to ``x`` and stores the result in ``out``."""
l2_indices = x.ufuncs.absolute().asarray() < epsilon + self.sigma
l2_indices = np.float32(l2_indices)

pos_indices = np.float32(x.asarray() > 0)

tmp = (epsilon / (epsilon + self.sigma) * x * l2_indices +
(x - self.sigma) * (1 - l2_indices) * pos_indices +
(x + self.sigma) * (1 - l2_indices) * (1 - pos_indices))

out.assign(tmp)

return out

return ProximalHuberNorm


if __name__ == '__main__':
# pylint: disable=wrong-import-position
from odl.util.testutils import run_doctests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
func_params = ['l1', 'l2', 'l2^2', 'kl', 'kl_cross_ent', 'const',
'groupl1-1', 'groupl1-2',
'nuclearnorm-1-1', 'nuclearnorm-1-2', 'nuclearnorm-1-inf',
'quadratic', 'linear']
'quadratic', 'linear', 'huber']

func_ids = [" functional='{}' ".format(p) for p in func_params]

Expand Down Expand Up @@ -73,6 +73,8 @@ def functional(request, linear_offset, quadratic_offset, dual):
vector=space.one(), constant=0.623)
elif name == 'linear':
func = odl.solvers.QuadraticForm(vector=space.one(), constant=0.623)
elif name == 'huber':
func = odl.solvers.HuberNorm(space, epsilon=0.162)
else:
assert False

Expand Down Expand Up @@ -126,6 +128,14 @@ def test_proximal_defintion(functional, stepsize):
pytest.skip('functional has no call method')
return

# Special case not covered above. Cconj not implemetned is not seen through
# the quadratic perturbation
if (isinstance(functional, odl.solvers.FunctionalQuadraticPerturb) and
isinstance(functional.functional,
FunctionalDefaultConvexConjugate)):
pytest.skip('functional has no call method')
return

# No implementation of the proximal for convex conj of
# FunctionalQuadraticPerturb unless the quadratic term is 0.
if (isinstance(functional, odl.solvers.FunctionalQuadraticPerturb) and
Expand Down Expand Up @@ -189,6 +199,23 @@ def test_convex_conj_defintion(functional):
pytest.skip('functional has no call')
return

# Special case not covered above. Cconj not implemetned is not seen through
# the quadratic perturbation
if (isinstance(functional, odl.solvers.FunctionalQuadraticPerturb) and
isinstance(functional.functional,
FunctionalDefaultConvexConjugate)):
pytest.skip('functional has no call')
return

# Same but with translation on the functional
if (isinstance(functional, odl.solvers.FunctionalTranslation) and
(isinstance(functional.functional,
FunctionalDefaultConvexConjugate) or
isinstance(functional.functional.convex_conj,
FunctionalDefaultConvexConjugate))):
pytest.skip('functional has no call')
return

f_convex_conj = functional.convex_conj
if isinstance(f_convex_conj, FunctionalDefaultConvexConjugate):
pytest.skip('functional has no convex conjugate')
Expand Down
4 changes: 3 additions & 1 deletion odl/test/solvers/functional/functional_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def space(request, fn_impl):
func_params = ['l1 ', 'l2', 'l2^2', 'constant', 'zero', 'ind_unit_ball_1',
'ind_unit_ball_2', 'ind_unit_ball_pi', 'ind_unit_ball_inf',
'product', 'quotient', 'kl', 'kl_cc', 'kl_cross_ent',
'kl_cc_cross_ent']
'kl_cc_cross_ent', 'huber']
func_ids = [" functional='{}' ".format(p) for p in func_params]


Expand Down Expand Up @@ -91,6 +91,8 @@ def functional(request, space):
func = odl.solvers.functional.KullbackLeiblerCrossEntropy(space)
elif name == 'kl_cc_cross_ent':
func = odl.solvers.KullbackLeiblerCrossEntropy(space).convex_conj
elif name == 'huber':
func = odl.solvers.HuberNorm(space, epsilon=0.1)
else:
assert False

Expand Down