Information Theory and Numerical Stability
Entropy, cross-entropy and KL, stable softmax and log-sum-exp, float formats, PCA and distance choice.
- Concepts and terms
- Tasks and commands
- Annotated code
- Diagrams
- Defaults and numbers
- Troubleshooting
- Questions and tips
Information formulas#
- Surprise
- I(x) = -\log p(x). Base 2 gives bits. Base e gives nats.
- Entropy
- H(P) = -\sum_x p(x) \log p(x). Fair coin 1 bit. A 99% coin 0.08 bits.
- Cross-entropy
- H(P,Q) = -\sum_x p(x) \log q(x). A one-hot label gives -\log q(\text{true class}).
- KL divergence
- D_{KL}(P \Vert Q) = H(P,Q) - H(P). It is not symmetric.
- Mutual information
- I(X;Y) = H(X) + H(Y) - H(X,Y). Zero means independent.
- Conditional entropy
- H(Y \mid X) = H(X,Y) - H(X). Decision tree splits minimize it.
- Perplexity
- e^{H(P,Q)} in nats. Perplexity 50 is a uniform pick from 50 tokens.
- Label smoothing
- (1-\epsilon)\,\text{hard} + \epsilon / K. With 0.1 and 4 classes the true class gets 0.925.
- Units
- 1 nat is 1.4427 bits. PyTorch and TensorFlow use nats.
From Lesson 01.09
Stable loss path#
Stable softmax, LSE and cross-entropy#
def logsumexp_stable(values): c = max(values)largest exponent becomes 0 return c + math.log(sum(math.exp(v - c) for v in values))def log_softmax_stable(logits): c = max(logits) lse = c + math.log(sum(math.exp(z - c) for z in logits)) return [z - lse for z in logits]log probs, no overflowdef cross_entropy_stable(true_class, logits): log_probs = log_softmax_stable(logits) return -log_probs[true_class]never calls log(0)Tasks in code#
| Task | Code |
|---|---|
| Entropy in bits | entropy(probs, |
| KL divergence | kl_divergence(p, |
| Perplexity from mean CE | perplexity(avg_cross_entropy, |
| Safe softmax | softmax_stable(logits) |
| Numerical gradient | numerical_gradient(f, |
| Compare to backprop | check_gradient(analytical, |
| Clip the gradient norm | clip_by_norm(gradients, |
| Variance with a large mean | welford_variance(values) |
From 01.09 code/information_theory.py, 01.13 code/numerical.py
Float formats and limits#
| Setting | Default | Note |
|---|---|---|
| float64 max | 1.8e308 | 11 exponent bits, ~15-16 digits |
| float32 max | 3.4028235e+38 | 8 exponent bits, ~7-8 digits |
| float32 min normal | 1.175e-38 | Denormals reach 1.401e-45 |
| float32 exp overflow | exp(89.0) | Gives inf. exp(88.7) still fits |
| float32 exp underflow | exp(-104) | Gives 0.0 |
| float16 max | 65,504 | Gradients below ~6e-8 become 0 |
| bfloat16 max | 3.4e38 | 8 exponent bits, 7 mantissa bits |
| float32 machine epsilon | 1.19e-7 | Smallest e with 1.0 + e != 1.0 |
From Lesson 01.13
Knobs and thresholds#
| Setting | Default | Note |
|---|---|---|
Gradient check h | 1e-5 | Typical range 1e-5 to 1e-7 |
| Gradient check pass | 1e-7 | Above 1e-3 something is wrong |
max_norm | 1.0 | Transformers. 0.5 for RL, 5.0 for simple nets |
LayerNorm epsilon | 1e-5 | Prevents division by zero |
| Epsilon in log and divide | 1e-8 | log(x + 1e-8) |
| t-SNE perplexity | 5-50 | The lesson uses 30 |
UMAP n_neighbors | 15 | With min_dist 0.1 |
| PCA cumulative variance | 0.95 | Or a 90-95% threshold |
From Lesson 01.13, Lesson 01.10
Mixed precision with loss scaling#
From Lesson 01.13
Numerical bugs#
| Symptom | Cause | Fix |
|---|---|---|
| Loss is NaN after a few epochs | Logits too large, softmax overflowed | Stable softmax, lower LR, clip gradients |
| Loss stuck at log(num_classes) | Near-uniform outputs, no learning | Check labels, loss and dead ReLUs |
| Val accuracy 1-3% low | Mixed precision without loss scaling | Dynamic loss scaling or bfloat16 |
| Gradient norm 0.0 in some layers | Dead ReLUs or float16 underflow | LeakyReLU or GELU, gradient scaling |
| Results differ between GPUs | Float addition is not associative | torch.use_deterministic_algorithms(True) |
exp() returns inf in the loss | Raw logits without max subtraction | torch.nn.functional.log_softmax() |
| Diverges after float16 switch | Grads below 6e-8, values above 65,504 | AMP with loss scaling, or bfloat16 |
| Wrong variance for large values | Catastrophic cancellation | welford_variance or center the data |
Pick a distance#
| Command | What it does |
|---|---|
cosine_similarity(a, | Text and embeddings. Ignores magnitude |
dot_product(a, | Recommendation. Magnitude encodes popularity |
l2_distance(a, | Pixels and spatial data at comparable scale |
l1_distance(a, | Sparse high-dim features. Robust to outliers |
linf_distance(a, | Worst-case deviation in one dimension |
mahalanobis_distance(x, | Outliers with correlated features |
jaccard_similarity(set_a, | Sets and tags. IoU for segmentation |
edit_distance(s1, | Strings. Insert, delete or substitute |
Reduce dimensions#
- Curse of dimensionality
- at 1000 dimensions the max/min distance ratio is ~1.02.
- PCA
- center, covariance,
np.linalg.eigh, sort by eigenvalue, keep the top k. - Explained ratio
- one eigenvalue divided by the sum of all eigenvalues.
- Pick k
- keep 90-95% of variance, find the elbow, or sweep k on the task.
- Reconstruction error
- the sum of the dropped eigenvalues. High error marks an outlier.
- t-SNE
- keeps local neighbors. Stochastic and O(n^2). Cluster gaps mean nothing.
- UMAP
- faster than t-SNE. It keeps more global structure.
- Kernel PCA
- PCA in a kernel feature space. RBF separates concentric circles.
Questions#
- Why subtract the max?
- The largest term becomes exp(0) = 1. The sum is at least 1.
- Why bfloat16 for training?
- It has the float32 range. Loss scaling is usually unnecessary.
- Why minimize cross-entropy?
- It equals negative log-likelihood. H(P) is constant.
- Why is KL not a distance?
- It is not symmetric and fails the triangle inequality.
- Why Wasserstein for GANs?
- It gives gradients when distributions do not overlap.
- Why does L1 give sparse weights?
- Loss contours touch the L1 diamond at a corner.
- Why centered differences?
- The error is O(h^2). Forward differences give O(h).
From Lesson 01.13, Lesson 01.14, Lesson 01.09
Tips#
- Do not compare floats with
==. Usemath.isclose(). - Check for nan and inf after each forward pass while you debug:
math.isfinite(x). - Clamp inputs to exp:
exp(clamp(x, -80, 80)). - Clip by norm with
torch.nn.utils.clip_grad_norm_(). It keeps the gradient direction. - Check each new layer or loss with
torch.autograd.gradcheck(). - With L2-normalized embeddings, cosine and dot product give the same result.
From Lesson 01.13, Lesson 01.14