← All posts
Deep Learning · Activation Functions · Part 3 of 9

Stable Sigmoid: Avoiding Overflow in Exponentials, Log-Sigmoid, and Losses

In exact real arithmetic, $\sigma(z)=1/(1+e^{-z})$ is always well-defined and strictly between zero and one. In floating-point arithmetic, the intermediate exponential $e^{-z}$ can overflow long before the final output should have been a harmless number near zero or one.

Where the naive formula fails

Every IEEE-754 floating-point format has a largest finite value, so $\exp(u)$ overflows to $+\infty$ above a format-dependent threshold: roughly $u>11.09$ in float16, $u>88.72$ in float32, and $u>709.78$ in float64.

When a logit is moderately negative—say $z=-100$ in float32 or $z=-1000$ in float64—evaluating $1/(1+e^{-z})$ asks for $e^{+100}$ or $e^{+1000}$, raising an overflow warning. Conversely, the algebraically equivalent fraction $e^z/(1+e^z)$ is safe for negative $z$, but overflows in both numerator and denominator for large positive $z$, producing $\infty/\infty=\mathrm{NaN}$.

Keep the exponent nonpositive

Multiply the numerator and denominator of $1/(1+e^{-z})$ by $e^z$ to get $e^z/(1+e^z)$. Choosing the first form when $z\geq0$ and the second when $z<0$ ensures that the exponential argument is never positive:

$$\sigma(z)=\begin{cases}\dfrac{1}{1+e^{-z}},&z\geq0,\\[8pt]\dfrac{e^z}{1+e^z},&z<0.\end{cases}$$

In both branches, the exponent is $-|z|\leq0$, so $e^{-|z|}$ lies in $(0,1]$ and never overflows in any floating-point precision. The denominator $1+e^{-|z|}$ stays between $1$ and $2$. Using $\tanh(z/2)$ expresses the same stabilization via the half-angle identity $\sigma(z)=\tfrac{1}{2}[1+\tanh(z/2)]$.

The derivative also benefits from writing the exponential in terms of $-|z|$:

$$\sigma^\prime(z)=\frac{e^{-|z|}}{\left(1+e^{-|z|}\right)^2}.$$

When $z$ is positive and moderately large (for example $z=20$ in float32), $\sigma(z)$ rounds to $1.0$, so $\sigma(z)(1-\sigma(z))$ rounds directly to zero. Evaluating $e^{-|z|}/(1+e^{-|z|})^2$ preserves non-zero tail gradients until $e^{-|z|}$ itself underflows.

Why log-sigmoid needs its own primitive

Making $\sigma(z)$ overflow-free does not make $\log(\sigma(z))$ safe. At $z=-100$ in float32 (or $z=-1000$ in float64), even the stable branch $e^z/(1+e^z)$ underflows to $0.0$, and taking $\log(0.0)$ returns $-\infty$. Yet the true value of $\log\sigma(z)$ is smooth and nearly linear in $z$:

$$\begin{aligned}\log\sigma(z)&=-\log\left(1+e^{-z}\right)\\&=-\max(0,-z)-\log\left(1+e^{-|z|}\right).\end{aligned}$$

For $z=-100$, $-\max(0,100)=-100$ and $\log(1+e^{-100})\approx0$, so $\log\sigma(-100)\approx-100$ with no overflow or $\log(0)$. By symmetry, $1-\sigma(z)=\sigma(-z)$, so $\log(1-\sigma(z))=\log\sigma(-z)$.

Stable values across extreme and moderate inputs (rounded to four decimals).
Quantity−100−101100
$\sigma(z)$0.00000.26890.50000.73111.0000
$\log\sigma(z)$-100.0000-1.3133-0.6931-0.31330.0000
BCE ($y=1$)100.00001.31330.69310.31330.0000

Combine sigmoid and binary cross-entropy

For a binary target $y\in[0,1]$ and logit $z$, binary cross-entropy is $-y\log\sigma(z)-(1-y)\log(1-\sigma(z))$. Substitute $\log\sigma(z)=-\log(1+e^{-z})$ and $\log(1-\sigma(z))=-z-\log(1+e^{-z})$ to obtain a single stable formula:

$$\begin{aligned}L(z,y)&=\max(z,0)-zy\\&\quad+\log\left(1+e^{-|z|}\right).\end{aligned}$$

This is the exact identity inside PyTorch's BCEWithLogitsLoss. The exponential only ever sees $-|z|\leq0$, and $\operatorname{log1p}(e^{-|z|})$ stays accurate when $e^{-|z|}$ is tiny.

Advantages and disadvantages

Compare ways of evaluating sigmoid and its log-loss in finite precision.
FormulationAdvantagesDisadvantages
Naive $1/(1+e^{-z})$Matches the textbook formula directly and is accurate for nonnegative $z$.Overflows in $e^{-z}$ for large negative $z$; computing $\log(\sigma(z))$ afterward still fails when $\sigma(z)$ rounds to 0 or 1.
Two-branch $\sigma(z)$ (expit)Keeps the exponential argument nonpositive ($-|z|\leq0$), avoiding overflow across all inputs.Requires a branch or mask; extreme negative inputs still underflow to $0.0$, so taking $\log$ afterward can still produce $-\infty$.
Log-space $\log\sigma(z)$ and fused BCEPreserves linear tail behavior ($\approx z$ for $z\ll0$) and avoids both $e^{-z}$ overflow and $\log(0)$ underflow.Must receive raw logits $z$ directly rather than pre-computed probabilities $p=\sigma(z)$.

Reference implementation

import numpy as np

def stable_sigmoid(z):
    z = np.asarray(z, dtype=float)
    pos = z >= 0
    out = np.empty_like(z)
    out[pos] = 1.0 / (1.0 + np.exp(-z[pos]))
    ez = np.exp(z[~pos])
    out[~pos] = ez / (1.0 + ez)
    return out

def log_sigmoid(z):
    return -np.logaddexp(0.0, -np.asarray(z, dtype=float))

def bce_with_logits(z, y):
    z = np.asarray(z, dtype=float)
    return np.maximum(z, 0.0) - z * y + np.log1p(np.exp(-np.abs(z)))

In SciPy, scipy.special.expit implements the stable sigmoid primitive used by SiLU and Swish and SwiGLU. Next, ReLU and its variants replace bounded exponential tails with piecewise linear branches.