Repository navigation
Conversation
…cation` The `else` branch of `_modifiedHuberLoss` was missing a factor of `2` and was applied unconditionally for `d = y*f(x) < 1`, including the `d >= 1` region where the loss (and its gradient) is exactly zero. Per the modified Huber loss definition (Wikipedia, "Variant for classification"; scikit-learn's `_sgd_fast.pyx.tp` `ModifiedHuber`), the gradient of the loss w.r.t. the decision value `p = y*f(x)` is: - `p >= 1`: `0` - `-1 <= p < 1`: `-2*y*(1-p)` - `p < -1`: `-4*y` The weight update is `w += eta * (-dloss/dp) * x`, so the middle region needs `2*eta*(y - p*y)`, not `eta*(y - p*y)`, and the `p >= 1` region needs no update at all. Fixes stdlib-js#13111
|
Hello! Thank you for your contribution to stdlib. We noticed that the contributing guidelines acknowledgment is missing from your pull request. Here's what you need to do:
This acknowledgment confirms that you've read the guidelines, which include:
We can't review or accept contributions without this acknowledgment. Thank you for your understanding and cooperation. We look forward to reviewing your contribution! |
|
Gentle ping on #15317 — it's been 9 days. Is there anything you'd like changed before it can be reviewed? Happy to rebase or split it if that helps. |
|
@nakul-krishnakumar Would you mind doing an initial review of this PR? Cheers! |
| if ( d < -1.0 ) { | ||
| this._add( x, 4.0*eta*y ); | ||
| } else { | ||
| this._add( x, eta*( y-(d*y) ) ); | ||
| } else if ( d < 1.0 ) { | ||
| this._add( x, 2.0*eta*( y-(d*y) ) ); | ||
| } |
There was a problem hiding this comment.
This does not look correct to me as here else if comes after an else. According to sklearn, the correct branching should be:
if ( d >= 1.0 ) {
// no update
} else if ( d >= -1.0 ) {
this._add( x, 2.0*eta*( y-(d*y) ) );
} else {
this._add( x, 4.0*eta*y );
}To make it simpler we can do this:
if ( d < -1.0 ) {
this._add( x, 4.0*eta*y );
} else if ( d < 1.0 ) {
this._add( x, 2.0*eta*( y-(d*y) ) );
}There was a problem hiding this comment.
Another reference is our @stdlib/ml/base/loss/float64/modified-huber-gradient implementation.
| // FUNCTIONS // | ||
|
|
||
| /** | ||
| * Returns a model configured to use the modified Huber loss function with a constant learning rate and no regularization. | ||
| * | ||
| * @private | ||
| * @param {number} eta - learning rate | ||
| * @returns {Model} model instance | ||
| */ | ||
| function createModel( eta ) { | ||
| return new Model( 1, { | ||
| 'lambda': 0.0, | ||
| 'learningRate': [ 'constant', eta ], | ||
| 'loss': 'modifiedHuber', | ||
| 'intercept': false | ||
| }); | ||
| } | ||
|
|
||
|
|
There was a problem hiding this comment.
I believe this is not the convention we follow, instead we can create the model object in each test case itself. For better clarity refer this: ml/incr/kmeans
| }); | ||
|
|
||
| // TODO: add tests | ||
| tape( 'the modified Huber loss update includes a `2` factor when `-1 <= y*f(x) < 1` (see stdlib-js/stdlib#13111)', function test( t ) { |
There was a problem hiding this comment.
Instead of keeping three separate tests for just modified huber loss, I would suggest to write a single test, maybe something like:
tape( 'the function supports modified Huber loss', function test( t ) {
// test case 1
// test case 2
// test case 3
}And this test can have multiple test cases, each testing different branches of the loss function. I would also recommend to only add this test as part of this PR and then later open a new PR to add all the other tests for this package.
Address review feedback: create each model inside the test case and cover the three branches of the loss in a single test.
|
@nakul-krishnakumar thanks for the review.
|
Coverage Report
The above coverage report was generated for the changes in this PR. |
| }; | ||
| x = array( new Float64Array( [ 1.0 ] ) ); | ||
|
|
||
| // Case 1: `-1 <= y*f(x) < 1` (quadratically smoothed region), where the update is `2*eta*( y - d*y )` (see stdlib-js/stdlib#13111): |
There was a problem hiding this comment.
| // Case 1: `-1 <= y*f(x) < 1` (quadratically smoothed region), where the update is `2*eta*( y - d*y )` (see stdlib-js/stdlib#13111): | |
| // Case 1: `-1 <= y*f(x) < 1` (quadratically smoothed region): |
| model.update( x, 1 ); | ||
| t.strictEqual( model._weights[ 0 ], 2.0, 'returns expected value' ); // eslint-disable-line no-underscore-dangle | ||
|
|
||
| // Case 3: `y*f(x) < -1`, where the update is `4*eta*y`: |
There was a problem hiding this comment.
| // Case 3: `y*f(x) < -1`, where the update is `4*eta*y`: | |
| // Case 3: `y*f(x) < -1`: |
Oh right! My bad about the first point. 😅 cc: @kgryte |
Co-authored-by: Nakul Krishnakumar <nakul-krishnakumar@users.noreply.github.com>
Description
Fixes #13111. The
elsebranch of_modifiedHuberLossinml/incr/binary-classification'sModelwas missing a factor of2in the "quadratically smoothed" region, and was being applied unconditionally ford = y*f(x) < 1— including thed >= 1region, where the modified Huber loss (and therefore its gradient) is exactly zero.This goes slightly beyond what #13111 literally flagged (just the missing
2): I independently re-derived the three-region gradient against the cited references before changing anything, since applying only the factor-of-2 fix without also bounding the branch atd < 1.0would still be wrong ford >= 1.Per the modified Huber loss definition (Wikipedia, "Variant for classification"; scikit-learn's
_sgd_fast.pyx.tpModifiedHuber.dloss), the gradient of the loss w.r.t. the decision valuep = y*f(x)is:p >= 1:0-1 <= p < 1:-2*y*(1-p)p < -1:-4*ySince the weight update here is
w += eta * (-dloss/dp) * x, the middle region needs2*eta*(y - p*y)(noteta*(y - p*y)), and thep >= 1region needs no update at all. Thep < -1branch (4.0*eta*y) was already correct and is unchanged.Testing
Added three new test cases to
test/test.model.jscovering each region (p < -1,-1 <= p < 1,p >= 1), confirming:p >= 1,p < -1branch is unaffected.eslinton both touched files is clean (aside from a pre-existing, unrelated spelling warning on "pegasos").Related Issues
Fixes #13111
Checklist
AI Assistance
If you answered "yes" above, how did you use AI assistance?
Disclosure
This PR was written primarily by Claude Code.
@stdlib-js/reviewers