The problem
Binary cross-entropy is the loss for yes/no predictions: is this answer correct, is this text safe, does the reward model prefer answer A over answer B. The network outputs one logit per question, any real number, and the probability of "yes" is its sigmoid:
where is the logit and is a number between 0 and 1: large positive logits give almost 1, large negative ones almost 0, and .
The target is 1 for "yes" and 0 for "no". The loss is minus the log of the probability given to the right answer: when , and when . In one formula:
where one of the two terms is always 0: with only the first remains, with only the second.
Example. , so .
- If : . The model said "probably yes", and it was yes: small loss.
- If : . It said "probably yes", and it was no: large loss.
Why the formula breaks, and the fix
For large logits the sigmoid rounds to exactly 1: is 1.0 in float64. With the loss then computes . The same loss can be rewritten without ever computing (derivation on the theory page):
where is the absolute value of . The exponent is never positive, so is between 0 and 1: nothing overflows, and the log never sees 0.
Example.
- , : , the same as above.
- , : . The naive formula returned
inf.
The gradient
As for cross-entropy, it is prediction minus target. With the mean over elements:
where and are element of the logits and targets, and is the number of elements.
Example.
x = [2, -3, 0],y = [1, 1, 0.5], so .
- Losses: , , ; the mean is about .
- Gradient: , minus , divided by 3: about . The second logit gets the biggest push: it said "no" with confidence, and the answer was yes.
The third target, , is a soft target: "half yes". Any number between 0 and 1 works in the same formulas.
What to implement
For arrays x (logits) and y (targets between 0 and 1) of the same shape:
sigmoid(x): elementwise, finite for anyx.bce_with_logits(x, y): the mean of over all elements.bce_with_logits_backward(x, y): the gradient with respect tox.
Requirements
bce_with_logits(np.array([-800.0]), np.array([1.0]))is , notinfornan.- Soft targets must work.
- Use
np.log1p(t)for : it stays accurate when is tiny. For examplenp.log(1 + 1e-20)returns0.0, because1 + 1e-20rounds to1, whilenp.log1p(1e-20)returns the correct1e-20.
Hint
For the sigmoid, is exact and never computes a huge exponential. (1 / (1 + np.exp(-x)) also gives the right value, but on a desktop NumPy warns that np.exp(800) overflowed.)