Cross-Entropy Loss › Code Implementation

Michael BrenndoerferFebruary 21, 20268 min read
Language AI HandbookAll chapters
Part 53 of 67Evaluation Fundamentals
Chapter 406Cross-Entropy Loss › Code Implementation
Reading section 10 of 15 · Code Implementation · 8 minSection 10 of 15 · 8 min

Book contents

Section 10 of 15 · 8 min read

Cross-Entropy Loss › Code ImplementationOne section · Sections in this chapter10 / 15 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Let's implement cross-entropy calculations from scratch and using standard libraries. We'll demonstrate the relationship between manual calculation, PyTorch's implementation, and the conversion to perplexity.

In[6]:
Code
import numpy as np
import torch

# Set seed for reproducibility
np.random.seed(42)
torch.manual_seed(42)

We'll start with a basic implementation using NumPy to understand the mechanics, then show the optimized PyTorch version used in actual training loops.

In[7]:
Code
def cross_entropy_manual(y_true_idx, y_pred_probs, eps=1e-12):
    """
    Manual cross-entropy calculation.

    Args:
        y_true_idx: Integer index of true class
        y_pred_probs: Array of predicted probabilities (must sum to 1)
        eps: Small constant to avoid log(0)

    Returns:
        Cross-entropy in nats (natural log)
    """
    # Clip probabilities to avoid log(0)
    y_pred_clipped = np.clip(y_pred_probs, eps, 1.0 - eps)

    # Get probability assigned to true class
    prob_true = y_pred_clipped[y_true_idx]

    # Return negative log probability
    return -np.log(prob_true)


def perplexity_from_entropy(cross_entropy, base="e"):
    """Convert cross-entropy to perplexity."""
    if base == "2":
        return 2**cross_entropy
    else:
        return np.exp(cross_entropy)

Now let's test with our worked example from earlier:

Out[8]:
Console
Vocabulary: ['the', 'cat', 'sat', 'mat']
True token: 'sat' (index 2)
Model probabilities: {'the': np.float64(0.1), 'cat': np.float64(0.05), 'sat': np.float64(0.7), 'mat': np.float64(0.15)}

Cross-entropy: 0.3567 nats (0.5146 bits)
Perplexity: 1.4286

Uniform distribution cross-entropy: 1.3863 nats
Uniform perplexity: 4.0000 (should equal vocab size 4)

The manual calculation confirms our mathematical understanding. Now let's implement sequence-level evaluation using PyTorch, which handles batched operations efficiently:

In[9]:
Code
def evaluate_sequence(model_probs, true_indices):
    """
    Calculate cross-entropy and perplexity for a sequence.

    Args:
        model_probs: Tensor of shape (seq_len, vocab_size) with probability distributions
        true_indices: Tensor of shape (seq_len,) with true token indices

    Returns:
        Dictionary with cross_entropy (nats), bits_per_token, perplexity, and bpc
    """
    seq_len = len(true_indices)

    # Extract probabilities of true tokens
    true_probs = model_probs[torch.arange(seq_len), true_indices]

    # Cross-entropy (negative log likelihood averaged over sequence)
    ce = -torch.log(true_probs).mean()

    # Convert to bits
    ce_bits = ce / torch.log(torch.tensor(2.0))

    # Perplexity
    perp = torch.exp(ce)

    return {
        "cross_entropy_nats": ce.item(),
        "cross_entropy_bits": ce_bits.item(),
        "perplexity": perp.item(),
    }

Let's simulate a sequence evaluation with our example sentence "the cat sat mat":

Out[10]:
Console
Sequence evaluation results:
  Cross-entropy: 0.7925 nats
  Cross-entropy: 1.1434 bits/token
  Perplexity: 2.2090
  Bits per character: 0.3049 (assuming 15 chars)

PyTorch provides optimized implementations that handle numerical stability through log-softmax and avoid explicit probability calculations when working with logits directly:

In[11]:
Code
import torch.nn.functional as F

# PyTorch's cross_entropy function operates on logits, not probabilities
logits = torch.log(
    probs_tensor + 1e-12
)  # Convert back to log space for demonstration

# F.cross_entropy expects (N, C) for logits and (N,) for targets
# It combines log_softmax + nll_loss efficiently
pytorch_ce = F.cross_entropy(logits, true_indices)
Out[12]:
Console
PyTorch cross-entropy: 0.7925
Matches manual calculation: True

The close agreement between PyTorch's optimized implementation and our manual calculation confirms that both approaches correctly compute cross-entropy. The tiny numerical differences (if any) stem from floating-point precision variations in how probabilities are handled. This validation gives us confidence that our manual implementation correctly implements the mathematical definition, while PyTorch's version offers superior numerical stability and computational efficiency for production use.

Visualizing Loss Curves

Let's simulate training curves to demonstrate typical cross-entropy dynamics:

In[13]:
Code
import numpy as np


def simulate_training_curve(
    steps=1000, initial_loss=8.0, final_loss=2.5, noise=0.05
):
    """
    Simulate realistic training and validation loss curves.
    """
    # Training loss: smooth exponential decay with noise
    t = np.linspace(0, 1, steps)
    train_loss = final_loss + (initial_loss - final_loss) * np.exp(-5 * t)
    train_loss += np.random.normal(0, noise, steps) * (
        1 - 0.8 * t
    )  # Decreasing noise

    # Validation loss: similar but with gap and potential overfitting at end
    val_loss = (
        train_loss + 0.3 + 0.2 * np.maximum(0, t - 0.7) ** 2
    )  # Gap increases slightly at end
    val_loss += np.random.normal(0, noise * 1.5, steps)

    return train_loss, val_loss


train_loss, val_loss = simulate_training_curve()
steps = np.arange(len(train_loss))
Out[14]:
Visualization
Line plot of training and validation cross-entropy loss decreasing over 1000 training steps with a secondary perplexity axis on the right.
Simulated training dynamics showing cross-entropy loss decrease over 1000 steps. Training loss (solid) decays smoothly from roughly 8.0 nats toward a plateau near 2.5 nats, while validation loss (dashed) tracks slightly higher with increased variance throughout. The secondary right axis shows the corresponding perplexity values, illustrating how the exponential relationship compresses large perplexity differences at the start of training into a narrow band as the model improves.

The plot demonstrates how cross-entropy decreases during training while maintaining the exponential relationship with perplexity shown on the secondary axis. Notice how validation loss typically runs 10-30% higher than training loss due to the model not having seen that specific data during optimization.

Comparing Different Model Sizes

Let's examine how cross-entropy scales with model capacity, which reflects the power laws discussed in Power Laws in Deep Learning:

Out[15]:
Console
Model Size vs Cross-Entropy (simulated):
--------------------------------------------------
   0.1B params: CE = 6.185 nats, Perplexity = 485.3
   0.3B params: CE = 5.623 nats, Perplexity = 276.7
   0.8B params: CE = 5.301 nats, Perplexity = 200.6
   1.3B params: CE = 5.089 nats, Perplexity = 162.3
   2.7B params: CE = 4.814 nats, Perplexity = 123.3
   6.7B params: CE = 4.493 nats, Perplexity = 89.4
  13.0B params: CE = 4.272 nats, Perplexity = 71.7
 175.0B params: CE = 3.506 nats, Perplexity = 33.3
Out[16]:
Visualization
Scatter plot on a log x-axis showing simulated cross-entropy loss decreasing from about 6.2 nats at 100 million parameters to 3.5 nats at 175 billion parameters along a power-law curve.
Power-law scaling of cross-entropy loss with model parameter count, following Kaplan et al. scaling law predictions. In this simulation, loss decreases predictably from about 6.2 nats at 100 million parameters to 3.5 nats at 175 billion parameters. Each doubling of parameters yields a consistent fractional reduction in cross-entropy, illustrating why scaling remains an effective strategy for improving language model performance.

This demonstrates the predictable relationship between model scale and cross-entropy: with the simulated exponent above, doubling model parameters yields a constant reduction in loss of about 5% in the relevant regime.