Skip to content

Commit

Permalink
Merge 4bb0708 into bc50457
Browse files Browse the repository at this point in the history
  • Loading branch information
matteofrigo committed Apr 8, 2020
2 parents bc50457 + 4bb0708 commit a7f9f82
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.rst
Expand Up @@ -75,6 +75,7 @@ To compose your objective, the following functions are included:
* TV-norm (eval, prox)
* Projection on the positive octant (eval, prox)
* Projection on the L2-ball (eval, prox)
* Structured sparsity (eval, prox)

Alternatively, you can easily define a custom function by implementing an
evaluation method and a proximal operator or gradient method:
Expand Down
1 change: 1 addition & 0 deletions doc/history.rst
Expand Up @@ -8,6 +8,7 @@ Unreleased
* Drop support of Python 3.4 and test with 3.7. Last version to support 2.7.
* Merged all the extra requirements in a single dev requirement.
* Addition of the proj_positive function
* Addition of the structured_sparsity function.

0.5.2 (2017-12-15)
------------------
Expand Down
76 changes: 75 additions & 1 deletion pyunlocbox/functions.py
Expand Up @@ -42,6 +42,7 @@
.. autosummary::
dummy
structured_sparsity
.. inheritance-diagram:: pyunlocbox.functions
:parts: 2
Expand Down Expand Up @@ -350,7 +351,7 @@ def cap(self, x):

class dummy(func):
r"""
Dummy function which returns 0 (eval, prox, grad).
Dummy function (eval, prox, grad).
This can be used as a second function object when there is only one
function to minimize. It always evaluates as 0.
Expand Down Expand Up @@ -990,3 +991,76 @@ def _prox(self, x, T):
niter))

return sol


class structured_sparsity(func):
r"""
Structured sparsity (eval, prox).
The structured sparsity term that is defined in the work of
Jenatton et al. 2011 `Proximal methods for hierarchical sparse coding
<https://hal.inria.fr/inria-00516723>`_.
.. math:: \Omega(x) = \lambda \cdot \sum_{g \in G} w_g \cdot \|x_g\|_2
See generic attributes descriptions of the
:class:`pyunlocbox.functions.func` base class.
Parameters
----------
lambda_ : float, optional
The scaling factor of the function that corresponds to :math:`\lambda`.
Must be a non-negative number.
groups: list of lists of integers
Each element encodes the indices of the vector belonging to a single
group. Corresponds to :math:`G`.
weights : array_like
Weight associated to each group. Corresponds to :math:`w_g`. Must have
the same length as :math:`G`.
Examples
--------
>>> from pyunlocbox import functions
>>> groups = [[0, 1], [3, 2, 4]]
>>> weights = [2, 1]
>>> f = functions.structured_sparsity(10, groups, weights)
>>> x = [2, 2.5, -0.5, 0.3, 0.01]
>>> f.eval(x)
69.86305169905782
>>> f.prox(x, 0.1)
array([0.7506099 , 0.93826238, 0. , 0. , 0. ])
"""

def __init__(self, lambda_=1, groups=[[]], weights=[0], **kwargs):
super(structured_sparsity, self).__init__(**kwargs)

if lambda_ < 0:
raise ValueError('The scaling factor must be non-negative.')
self.lambda_ = lambda_

if not isinstance(groups, list):
raise TypeError('The groups must be defined as a list of lists.')
self.groups = groups

if len(weights) != len(groups):
raise ValueError('Length of weights must be equal to number of '
'groups.')
self.weights = weights

def _eval(self, x):
costs = [w * np.linalg.norm(x[g])
for g, w in zip(self.groups, self.weights)]
return self.lambda_ * np.sum(costs)

def _prox(self, x, T):
gamma = self.lambda_ * T
v = x.copy()
for g, w in zip(self.groups, self.weights):
xn = np.linalg.norm(v[g])
r = gamma * w
if xn > r:
v[g] -= v[g] * r / xn
else:
v[g] = 0
return v
36 changes: 36 additions & 0 deletions pyunlocbox/tests/test_functions.py
Expand Up @@ -422,6 +422,42 @@ def test_proj_positive(self):
nptest.assert_equal(res[x > 0], x[x > 0]) # Positives are unchanged.
self.assertEqual(fpos.eval(x), 0)

def test_structured_sparsity(self):
"""
Test the structured sparsity function.
"""
# test init
# test parsing of lambda
with self.assertRaises(ValueError):
functions.structured_sparsity(-1, [[]], [0.0])
# test parsing of groups
with self.assertRaises(TypeError):
functions.structured_sparsity(1.0, 1, [0.0])
# test parsing of weights
with self.assertRaises(ValueError):
functions.structured_sparsity(1.0, [[1, 2], [3, 4]], [10.])

# test call of eval and prox
x = np.array([0.01, 0.5, 3, 4])
groups = [[0, 1], [2, 3]]
weights = np.array([10, .2])
f = functions.structured_sparsity(1, groups, weights)
# test eval
result = f.eval(x)
gt = (weights[0] * np.linalg.norm(x[groups[0]], 2) +
weights[1] * np.linalg.norm(x[groups[1]], 2))
self.assertAlmostEqual(result, gt)
# test prox
gt = x.copy()
# the first group has norm lower than the corresponding weight
gt[groups[0]] = 0
# the second group has norm higher than the corresponding weight
gt[groups[1]] -= (x[groups[1]] * weights[1] /
np.linalg.norm(x[groups[1]]))
prox = f.prox(x, 1)
np.testing.assert_almost_equal(prox, gt)

def test_capabilities(self):
"""
Test that a function implements the necessary methods. A function must
Expand Down

0 comments on commit a7f9f82

Please sign in to comment.