Skip to content

Added Huber loss function class - #2281

Open
msaca-okse wants to merge 11 commits into
TomographicImaging:masterfrom
msaca-okse:huberloss
Open

Added Huber loss function class#2281
msaca-okse wants to merge 11 commits into
TomographicImaging:masterfrom
msaca-okse:huberloss

Conversation

@msaca-okse

Copy link
Copy Markdown

Description

The Huber loss function behaves like the L2 loss close to 0, and like L1 loss for large values. The loss function is differentiable and can thus be used in GD or FISTA. It's an alternative to L2 loss that is more robust to extreme values. The implementation mirrors that of "cil.optimisation.functions.LeastSquares". The Huber loss has a parameter that selects when the cutoff between L2 and L1 loss happens.

Example Usage

G = IndicatorBox(lower=0.0)
huber_delta = 1.0
F = HuberLoss(A,b,huber_delta)
myFISTA = FISTA(f=F, 
                  g=G, 
                  initial=x0)

Contribution Notes

  • The content of this Pull Request (the Contribution) is intentionally submitted for inclusion in CIL (the Work) under the terms and conditions of the Apache-2.0 License
  • I confirm that the contribution does not violate any intellectual property rights of third parties

@msaca-okse

Copy link
Copy Markdown
Author

Fixes #2281

@github-project-automation github-project-automation Bot moved this to Todo in UM 2026 Jan 30, 2026
@gfardell gfardell added the community-contribution Community-submitted pull requests from contributors outside the core team. label Jan 30, 2026
@casperdcl casperdcl linked an issue Jan 30, 2026 that may be closed by this pull request
@casperdcl casperdcl moved this from Todo to In Progress in UM 2026 Jan 30, 2026
@casperdcl casperdcl removed this from UM 2026 Jan 30, 2026
@lauramurgatroyd

lauramurgatroyd commented May 1, 2026

Copy link
Copy Markdown
Member

Hi @msaca-okse I just added the license header and made some edits to ensure the documentation renders well:

image

I wonder whether c should be included in the above equation?

image

We will need to add unit tests before merging into CIL. Would you be interested in working on this or are you happy for the CIL developer team to add these?

@lauramurgatroyd lauramurgatroyd left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @msaca-okse, thanks for your contribution. I am now working on getting this in CIL. I am about to push some unit tests which I have developed.

Comment thread Wrappers/Python/cil/optimisation/functions/HuberLoss.py Outdated
Comment thread Wrappers/Python/cil/optimisation/functions/HuberLoss.py Outdated
Comment thread Wrappers/Python/cil/optimisation/functions/HuberLoss.py
lauramurgatroyd and others added 6 commits August 14, 2026 12:23
Signed-off-by: Laura Murgatroyd <60604372+lauramurgatroyd@users.noreply.github.com>
Signed-off-by: Laura Murgatroyd <60604372+lauramurgatroyd@users.noreply.github.com>
@lauramurgatroyd

Copy link
Copy Markdown
Member

I've updated the equation, rendered it looks like this:

image

@MargaretDuff MargaretDuff left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A really picky review - apologies.

However, I am happy with the code, the gradient and Lipscitz calculations!

Comment thread Wrappers/Python/cil/optimisation/functions/HuberLoss.py Outdated
Comment thread Wrappers/Python/cil/optimisation/functions/HuberLoss.py Outdated
Comment thread Wrappers/Python/cil/optimisation/functions/HuberLoss.py Outdated
Comment thread Wrappers/Python/cil/optimisation/functions/HuberLoss.py
Comment thread Wrappers/Python/cil/optimisation/functions/HuberLoss.py Outdated
Comment on lines +155 to +159
self._L = np.abs(self.c) * (self.A.norm() ** 2)
except AttributeError:
if self.A.is_linear():
Anorm = LinearOperator.PowerMethod(self.A, 10)[0]
self._L = np.abs(self.c) * (Anorm * Anorm)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that this is copied from Least squares but for a linear operator, calling A.norm() either accesses a stashed value or calls the power method itself. I don't understand in what cases the if statement on 157 will be triggered.

if self.A.is_linear():
Anorm = LinearOperator.PowerMethod(self.A, 10)[0]
self._L = np.abs(self.c) * (Anorm * Anorm)
else:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, not sure what this else will pick up. If the operator is not linear and calculate_norm is not defined, the user will get a NotImplementedError. Perhaps we catch and return that with a bit more of an explanation?

Comment on lines +172 to +173
D = DiagonalOperator(self.weight)
self._weight_norm = D.norm()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would do the same thing without building a new operator - I see arguments both ways

Suggested change
D = DiagonalOperator(self.weight)
self._weight_norm = D.norm()
self._weight_norm = self.weight.abs().max()

Co-authored-by: Margaret Duff <43645617+MargaretDuff@users.noreply.github.com>
Signed-off-by: Laura Murgatroyd <60604372+lauramurgatroyd@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community-contribution Community-submitted pull requests from contributors outside the core team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Huber loss function

5 participants