Skip to content

Fix: Correct YOLO loss gradient for x,y coordinates#3088

Merged
davisking merged 2 commits into
davisking:masterfrom
arrufat:fix-yolo-gradient
Jun 13, 2025
Merged

Fix: Correct YOLO loss gradient for x,y coordinates#3088
davisking merged 2 commits into
davisking:masterfrom
arrufat:fix-yolo-gradient

Conversation

@arrufat

@arrufat arrufat commented Jun 12, 2025

Copy link
Copy Markdown
Contributor

The gradient calculation for bounding box x and y coordinates in the loss_yolo_ helper impl::yolo_helper_impl::tensor_to_loss was missing a factor of 2.0. This factor arises from the chain rule due to the coordinate transformation output_scaled = network_output * 2.0 - 0.5, where the loss is calculated based on output_scaled, but the gradient needs to be with respect to network_output.

This commit multiplies the affected gradient terms by 2.0f to correctly apply the chain rule.

The gradient calculation for bounding box x and y coordinates in the
`loss_yolo_` helper `impl::yolo_helper_impl::tensor_to_loss` was
missing a factor of 2.0. This factor arises from the chain rule
due to the coordinate transformation `output_scaled = network_output * 2.0 - 0.5`,
where the loss is calculated based on `output_scaled`, but the gradient
needs to be with respect to `network_output`.

This commit multiplies the affected gradient terms by 2.0f to correctly
apply the chain rule.
@arrufat

arrufat commented Jun 12, 2025

Copy link
Copy Markdown
Contributor Author

I just asked Jules to review the YOLO loss for gradient errors or other kinds of mistakes, and it found this. So asked it to provide a more detailed explanation of why we need a second 2.0 factor:

  1. Raw Network Output for x: o_x = out_data[x_idx]
  2. Scaled Prediction (s_x): We transform o_x to get the value that's actually used for positioning: s_x = o_x * 2.0 - 0.5. The * 2.0 here is part of defining s_x.
  3. Target (t_x): This is the ideal value for s_x.
  4. Difference for Loss (diff): The loss function (SmoothL1) looks at the difference: diff = s_x - t_x = (o_x * 2.0 - 0.5) - t_x. The * 2.0 you see here is used to calculate this diff.
  5. Derivative of Loss w.r.t. diff (dL/ddiff): This is put_in_range(-1, 1, diff).

Now, the Chain Rule for the Gradient w.r.t. o_x (dL/do_x): We want to know how the Loss L changes if we wiggle the raw network output o_x. dL/do_x = (dL/ddiff) * (ddiff/do_x)

  • dL/ddiff is put_in_range(-1, 1, diff). This is the first part of the gradient expression.
  • ddiff/do_x is where the second factor of 2.0 comes from. We need to see how diff changes when o_x changes: diff = (o_x * 2.0 - 0.5) - t_x ddiff/do_x = d/do_x [(o_x * 2.0 - 0.5) - t_x] = 2.0

So, dL/do_x = put_in_range(-1, 1, (o_x * 2.0 - 0.5) - t_x) * 2.0.

In simpler terms:

  • The * 2.0 inside (o_x * 2.0 - 0.5 - t_x) helps calculate the error term that goes into the put_in_range function.
  • The * 2.0 outside (the one I added) accounts for the fact that o_x has a geared effect on this error term: changing o_x by a little bit changes the error term by twice that amount because of the initial scaling s_x = o_x * 2.0 - 0.5.

Think of it like a lever: o_x is your hand moving, s_x is the end of the lever moving twice as far. The force you feel back (the gradient) is also amplified by that lever ratio.

Either way I've just launched a training to check everything works (it should, since this change is equivalent to setting lambda_obj to 2), but still.

This kind of AI is great for code review because, even if it gets it wrong, it makes you think more deeply about confusing parts of the code.

@davisking

Copy link
Copy Markdown
Owner

Well, isn't it still wrong though?

So from the code:

                // The loss is the squared norm of the gradient
                loss += length_squared(rowm(mat(grad), n));

But that's not right. Since the derivative of length_squared(x) is 2*x. Similarly, the derivative of 0.5*length_squared(x) is x. So the integral of just x would be 0.5*length_squared(x), meaning the loss is really 0.5*length_squared(rowm(mat(grad), n)).

Comment thread dlib/dnn/loss.h
@arrufat

arrufat commented Jun 12, 2025

Copy link
Copy Markdown
Contributor Author

Right, I am hesitant to make the change, it's something that can be tuned with the lambdas.

But yes, I should probably add the 0.5 there, since after this change the loss almost doubles, so it would appear "transparent to the user".

What do you think?

  • either we add 2 in the x, y and 0.5 in the loss
  • or just close this PR.

@davisking

Copy link
Copy Markdown
Owner

Right, I am hesitant to make the change, it's something that can be tuned with the lambdas.

But yes, I should probably add the 0.5 there, since after this change the loss almost doubles, so it would appear "transparent to the user".

What do you think?

  • either we add 2 in the x, y and 0.5 in the loss
  • or just close this PR.

Yeah IDK. Whatever you think is best. You happy with what you have in the PR now? :D

@arrufat

arrufat commented Jun 13, 2025

Copy link
Copy Markdown
Contributor Author

The main issue I have is that the loss almost doubles, so, by introducing the 0.5 in the loss, we get similar values as before, maybe I will add that. It's just for display purposes, it doesn't change the current behavior.
BTW the training experiment ended with similar results, as expected (maybe the centers are a bit better positioned, because we are giving them more weight (the correct weight) than before.

@arrufat arrufat marked this pull request as ready for review June 13, 2025 01:50
@davisking

Copy link
Copy Markdown
Owner

Sounds good. Thanks for another PR :)

@davisking davisking merged commit 79e2d13 into davisking:master Jun 13, 2025
9 checks passed
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