Skip to content

Commit 8b4fbc0

Browse files
committed
Fix: use alpha + tau as the branch threshold in Huber.prox
1 parent e3d891a commit 8b4fbc0

2 files changed

Lines changed: 24 additions & 6 deletions

File tree

‎pyproximal/proximal/Huber.py‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,14 @@ class Huber(ProxOperator):
3636
3737
\prox_{\tau H_\alpha(\cdot)}(\mathbf{x}) =
3838
\begin{cases}
39-
\prox_{\frac{\tau}{2 \alpha} |x_i|^2}(x_i), & |x_i| \leq \alpha \\
40-
\prox_{\tau |x_i|}(x_i), & |x_i| > \alpha
39+
\prox_{\frac{\tau}{2 \alpha} |x_i|^2}(x_i), & |x_i| \leq \alpha + \tau \\
40+
\prox_{\tau |x_i|}(x_i), & |x_i| > \alpha + \tau
4141
\end{cases}
4242
43+
The branches switch at :math:`|x_i| = \alpha + \tau` rather than at
44+
:math:`|x_i| = \alpha`, as that is where the minimizer moves from the
45+
quadratic to the linear part of the Huber function.
46+
4347
"""
4448

4549
def __init__(self, alpha: float) -> None:
@@ -60,12 +64,11 @@ def __call__(self, x: NDArray) -> float:
6064
def prox(self, x: NDArray, tau: float) -> NDArray:
6165
y = np.zeros_like(x)
6266
xabs = np.abs(x)
63-
mask = xabs > self.alpha
67+
mask = xabs > self.alpha + tau
6468
y[~mask] = self.l2.prox(x[~mask], tau)
6569
y[mask] = self.l1.prox(x[mask], tau)
66-
# alternative from https://math.stackexchange.com/questions/1650411/
67-
# proximal-operator-of-the-huber-loss-function... currently commented
68-
# as it does not provide the same result
70+
# equivalent closed form, from https://math.stackexchange.com/questions/1650411/
71+
# proximal-operator-of-the-huber-loss-function
6972
# y = (1. - tau / np.maximum(np.abs(x), tau + self.alpha)) * x
7073

7174
return y

‎pytests/test_norms.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,21 @@ def test_Huber(par):
478478
assert moreau(hub, x, tau)
479479

480480

481+
@pytest.mark.parametrize("par", [(par1), (par2)])
482+
def test_Huber_prox(par):
483+
"""Huber proximal operator matches a brute-force minimizer, including for
484+
alpha < |x| <= alpha + tau where the quadratic branch still applies
485+
"""
486+
alpha, tau = par["sigma"], 1.5
487+
hub = Huber(alpha=alpha)
488+
489+
x = np.linspace(-2 * (alpha + tau), 2 * (alpha + tau), 41)
490+
ys = np.linspace(-3 * (alpha + tau), 3 * (alpha + tau), 120001)
491+
hys = np.where(np.abs(ys) <= alpha, ys**2 / (2 * alpha), np.abs(ys) - alpha / 2)
492+
expected = np.array([ys[np.argmin(tau * hys + (ys - xi) ** 2 / 2)] for xi in x])
493+
assert_array_almost_equal(hub.prox(x, tau), expected, decimal=3)
494+
495+
481496
@pytest.mark.parametrize("par", [(par1), (par2)])
482497
def test_HuberCircular(par):
483498
"""Circular Huber norm and proximal/dual proximal"""

0 commit comments

Comments
 (0)