Skip to content

Adding ridge regularization in Nonnegative Parafac HALS, with some needed documentation#603

Open
cohenjer wants to merge 18 commits intotensorly:mainfrom
cohenjer:ridge_ncp
Open

Adding ridge regularization in Nonnegative Parafac HALS, with some needed documentation#603
cohenjer wants to merge 18 commits intotensorly:mainfrom
cohenjer:ridge_ncp

Conversation

@cohenjer
Copy link
Copy Markdown
Contributor

@cohenjer cohenjer commented Jul 8, 2025

This PR adds the option to impose ridge regularization in nonnegative CP decomposition with the HALS algorithm. It uses the ridge coefficient already implemented in the HALS solver. Ridge regularization is very useful because it allows to ask for sparsity on a selected number of modes using l1 regularization and applying l2 regularization elsewhere. It also allows to prune spurious components.

The PR also adds a callback interface (without removing the return error API) to be able to track the loss function (which should account for regularization including sparsity). The documentation of the function has also been improve to show the exact cost function minimized by the HALS algorithm.

This has been tested locally, I added a unit test for ridge and sparse regularized nCPD.

I also corrected a typo from an earlier PR at line 320 of _nn_cp.py where nn_modes should have been used instead of n_modes (sparsity is not implemented for unconstrained modes)

@codecov
Copy link
Copy Markdown

codecov bot commented Jul 8, 2025

Codecov Report

❌ Patch coverage is 92.95775% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.17%. Comparing base (2c51714) to head (19a789c).
⚠️ Report is 13 commits behind head on main.

Files with missing lines Patch % Lines
tensorly/decomposition/_nn_cp.py 84.37% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #603      +/-   ##
==========================================
+ Coverage   88.07%   88.17%   +0.10%     
==========================================
  Files         132      132              
  Lines        7915     7985      +70     
==========================================
+ Hits         6971     7041      +70     
  Misses        944      944              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cohenjer
Copy link
Copy Markdown
Contributor Author

Any comments @aarmey @JeanKossaifi?

Copy link
Copy Markdown
Contributor

@aarmey aarmey left a comment

Choose a reason for hiding this comment

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

Thanks for adding this, @cohenjer! This looks good to me.

@cohenjer
Copy link
Copy Markdown
Contributor Author

Thanks @aarmey. I fixed the black issues, I had not updated black recently enough!

Copy link
Copy Markdown
Member

@JeanKossaifi JeanKossaifi left a comment

Choose a reason for hiding this comment

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

Thank you @cohenjer ! Looks great, it's super useful to have the ridge regularization option. I left some comments - mostly looking at minimizing code complexity where possible.

Comment thread tensorly/decomposition/_nn_cp.py Outdated
loss += ridge_coefficients[mode] ** 2 * tl.norm(
factor
) ** 2 + sparsity_coefficients[mode] * tl.sum(tl.abs(factor))
callback(cp_tensor, rec_error, loss)
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 is very specific computation just to pass to a callback no? What are the actual cases? I'd almost just have a conditional here if needed instead.

if verbose:
print("Received True from callback function. Exiting.")
break
if tol:
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.

Same here, we are adding a lot of complexity compared to the previous one that was just

if tol:
            factors_norm = cp_norm((weights, factors))
            iprod = tl.sum(tl.sum(mttkrp * factors[-1], axis=0))
            rec_error = (
                tl.sqrt(tl.abs(norm_tensor**2 + factors_norm**2 - 2 * iprod))
                / norm_tensor
             )

I'm not sure that extra complexity is needed, especially as it will make the code harder for others to read/maintain.

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.

Another comment is that this conflates the objective and the reconstruction error - to me the reconstruction error should not include the ridge regularization, unlike the objective function.

Copy link
Copy Markdown
Contributor Author

@cohenjer cohenjer Dec 18, 2025

Choose a reason for hiding this comment

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

Hi Jean,

To answer both your comments, indeed I was a bit conflicted here, because the previous code only returned the reconstruction error, but the algorithm is designed to minimize the objective function (including regularization). I did not want to break previous behavior and therefore still return the reconstruction error, but also return the loss (objective function) which I find also quite useful.

I guess we can also just return the factors and mttkrp and not the reconstruction error nor the loss, and let the user compute them manually if he wants to return the loss/rec error. This way in case a callback is used, the runtime would not necessarily be slower. The code would also be much simpler. We also already have error calc function knowing mttkrp somewhere in tensorly, so that would not be hard to use.

I could add the code for callback loss function in the documentation/examples.

To summarize:

  • Either we simplify the callback by just passing factors and mttkrp. It makes it slightly harder for the user to return rec error or objective function, but we can provide an example.
  • Or we pass the error and/or objective function as a callback argument, as I did, in which case we could, as you suggest, return only the rec_error. The code would be very similary to the previous version in that case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Another comment is that this conflates the objective and the reconstruction error - to me the reconstruction error should not include the ridge regularization, unlike the objective function.

This is actually exactly what I did I think!

@cohenjer
Copy link
Copy Markdown
Contributor Author

@JeanKossaifi I followed your recommendation and simplified all the callback calls to simply use the unnormalized reconstruction error in the signature, along with the CP tensor. This way the user can easily compute the loss function, and I believe it is consistent with the CP decomposition loss.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants