欢迎大家提Issue反馈问题或建议,建设推理王国!
Skip to content

Chapter 11: Error Is the Ladder of Progress (Backpropagation)

Mr. Pallas's Cat's Warm Welcome
In the previous chapter, we explored the simplest perceptual unit — the neuron — and saw how it integrates information and makes decisions. But a solitary neuron is limited; true intelligence requires connection and cooperation. Today, we answer a crucial question: when a neuron makes a mistake, how does it adjust itself? Error is not the endpoint, but the ladder of progress. Let's take it slow and explore the wisdom of backpropagation.


Core Question: Learning Begins with Error

"Professor," Piglet stared at the incorrect classification results on the screen, "I trained a neuron to recognize circles and squares, but it keeps misclassifying some diamonds as circles. It clearly 'sees' its own errors, yet doesn't know how to correct them."

It was a deep-autumn afternoon in Kangle Garden at Sun Yat-sen University. Sunlight slanted into the Black Stone House study, casting long shadows on the red-brick floor. Outside, the Pearl River glittered, and the occasional boat drifted slowly by. Inside, steam rose faintly from the gongfu tea set, and the wall clock ticked away, marking every moment of learning.

Little Seal said: "That's a classic problem. Historically, early neural network researchers faced the same dilemma: how to make a network learn from its errors?"

Mr. Pallas's Cat gently set down the teapot and smiled. "You've hit upon the core of the learning problem. Knowing the error is only the first step; knowing how to correct the error is true learning. Today, we explore this 'how' — the backpropagation algorithm."

The Dilemma of Learning: The Philosophy of Gradient Descent

Piglet walked to the whiteboard and drew a neuron diagram.

"Professor, a neuron has weights w and bias b. When it makes a mistake, we should adjust these parameters. But... in which direction? By how much?"

Little Seal added: "This reminds me of optimization problems in mathematics. To find the minimum of a function, we need to know the 'slope' at our current position — that is, the gradient."

Mr. Pallas's Cat nodded: "Yes, this is the core idea of gradient descent. Imagine you're standing on a mountain, surrounded by thick fog, unable to see the base. You can only feel the slope beneath your feet. To descend, you take a small step in the direction of the steepest slope."

He drew a valley diagram on the whiteboard:

        Peak
        /\
       /  \
      /    \
     /      \
    /        \  Valley (minimum)

"Our goal is to find the minimum of the loss function," Mr. Pallas's Cat explained. "The loss function L measures the gap between the network's prediction and the true value. Weights and biases are the 'positions' we can adjust."

Piglet thought: "So learning is... groping your way down a valley in the fog? Adjusting direction at each step based on the slope underfoot?"

"A concise analogy," Mr. Pallas's Cat smiled. "But there's a crucial question: neural networks have many layers, each with many neurons. How do we compute the 'slope' for every parameter?"


The Magic of the Chain Rule: Backward Propagation of Error

Outside, the sky grew dark, and warm lamplight filled the Black Stone House.

"Professor," Piglet asked, "if the error occurs at the output layer, how do we know how to adjust the parameters of the hidden layers?"

Mr. Pallas's Cat walked to the whiteboard and drew a simple two-layer network:

Input layer → Hidden layer → Output layer
x → h → y

"This is the key insight of backpropagation," he said. "Error propagates backward from the output layer to the input layer, spreading like ripples. The mathematical tool is the chain rule."

Little Seal mused: "The chain rule... from calculus? dzdx=dzdydydx?"

"Exactly," Mr. Pallas's Cat nodded. "In a neural network, the derivative of the output error with respect to a weight can be decomposed as: derivative of output w.r.t. intermediate variable × derivative of intermediate variable w.r.t. the weight."

He wrote the formula on the whiteboard:

Lw=Lyyzzw

where:

  • L is the loss function
  • y is the network output
  • z is the weighted sum
  • w is the weight parameter

Piglet studied the formula carefully: "This formula is like... error passing from back to front? Ly is the output layer error, multiplied by the activation function's derivative yz, then by the input's contribution zw?"

"Good understanding," Mr. Pallas's Cat said approvingly. "Ly tells us 'how far the prediction is from the target,' yz tells us 'how sensitive the activation function is here,' and zw tells us 'how much this weight contributed to the result.'"

The Three Steps of Backpropagation

Mr. Pallas's Cat summarized the three steps on the whiteboard:

  1. Forward pass: input data, compute the output of each layer, obtain the final prediction
  2. Error computation: compare prediction with true value, compute the loss function
  3. Backward pass: propagate error from output layer to input layer, compute gradients for every parameter

"These three steps form a cycle," Mr. Pallas's Cat said. "The forward pass is 'trying'; the backward pass is 'reflecting.' Through repeated cycles, the network gradually adjusts its parameters and reduces its errors."


Orthogonal Computation Graphs: Seeing the Reverse Flow of Error

Mr. Pallas's Cat turned on the projector, and a neat computation graph appeared on the screen.

Backpropagation Orthogonal Computation Graph

"This is the orthogonal computation graph for backpropagation," Mr. Pallas's Cat said, pointing. "Blue arrows represent the data flow of the forward pass; red arrows represent the error flow of the backward pass."

Piglet studied the bidirectional arrows carefully: "This graph is... bidirectional? Data flows left to right, error flows right to left?"

"Exactly," Mr. Pallas's Cat explained. "The orthogonal computation graph clearly shows the symmetry of backpropagation: the forward pass computes outputs; the backward pass computes gradients."

Little Seal mused: "Every computation node in the graph needs to save intermediate results from the forward pass so that gradients can be computed during the backward pass?"

"Yes," said Mr. Pallas's Cat. "This is the memory cost of backpropagation. To compute gradients, we need to remember the 'footprints' of the forward pass."

Piglet studied the computation graph intently: "Professor, some nodes in this diagram are marked with the '∂' symbol — does that mean partial derivatives?"

"Yes," Mr. Pallas's Cat said, pointing to key nodes in the diagram. "The core of backpropagation is computing the partial derivative of the loss function with respect to each parameter. These partial derivatives tell us: if we slightly increase this parameter, how will the loss change?"

Learning Rate: The Wisdom of Step Size

Mr. Pallas's Cat drew a steep slope on the whiteboard.

"Suppose the slope is very steep," he said. "If you take too big a step, you might overshoot — even run to the other side of the mountain. If the step is too small, the descent will be very slow."

Piglet grasped the analogy: "So the learning rate is the 'step size'? Too big may cause oscillation; too small may be too slow?"

"Exactly," Mr. Pallas's Cat nodded. "The learning rate η is the key hyperparameter of gradient descent. The update formula is:"

wwηLw

Little Seal added: "Historically, the choice of learning rate has always been empirical. Modern optimizers (like Adam) dynamically adjust the learning rate."

"Yes," said Mr. Pallas's Cat. "But today we first understand the basic principle: update parameters along the negative gradient direction with an appropriate step size."


Mental Model: Error as Teacher

Little Seal took an educational psychology book from the shelf. "Professor, this reminds me of the concept of 'formative assessment' in education."

"Good connection," said Mr. Pallas's Cat. "Backpropagation realizes 'formative assessment' for machine learning."

He wrote the mental models on the whiteboard:

Mental Model: Error-Driven Learning

  1. Trial-and-error learning: make a prediction through the forward pass, receive error feedback
  2. Error analysis: backpropagation analyzes the source and contribution of the error
  3. Parameter adjustment: adjust internal representations based on error analysis
  4. Progressive optimization: gradually approach the optimal solution through many iterations

"These four mechanisms," Mr. Pallas's Cat explained, "correspond to the core process of human learning: try, reflect, adjust, improve."

Piglet thought: "So neural network learning and human learning have a similar structure? We all learn from mistakes, all need to reflect on the causes of error, all need to adjust our 'internal models'?"

"Yes," Mr. Pallas's Cat answered. "This is the profound insight of backpropagation: learning is essentially error-driven model revision. For both machines and humans, error is not failure, but an opportunity for progress."

The Challenge of Local Minima vs. Global Optimum

Mr. Pallas's Cat drew multiple valleys on the whiteboard:

    ___     ___
   /   \___/   \___
  /                \__

"In reality, the loss function landscape is complex," he said. "There are multiple 'valleys' (local minima). Gradient descent may get trapped in a small valley, missing a deeper one."

Little Seal mused: "So this is the 'local minimum' problem? The network finds a 'pretty good' solution, but not the 'best' one?"

"Exactly," said Mr. Pallas's Cat. "This is an inherent limitation of gradient descent. In practice, we use strategies like random initialization, momentum, and stochastic gradient descent to mitigate it."

Piglet asked: "But... how do we know we've found the 'best' solution?"

Mr. Pallas's Cat smiled: "That's a profound question. In practice, we don't know whether we've found the global optimum. We can only ensure we've found a 'good enough' solution. This leads to an important philosophy of machine learning: pursue practicality, not perfection."


Key Takeaways

Mr. Pallas's Cat's Summary: The Wisdom of Backpropagation

  1. The philosophy of gradient descent: learning is the process of finding the minimum along the negative gradient direction of the loss function — embodying "small steps, iterative optimization" as a scientific methodology
  2. The magic of the chain rule: backpropagation uses the calculus chain rule to distribute output error backward to every parameter, enabling end-to-end learning in deep networks
  3. Symmetric computation structure: forward pass (data flow) and backward pass (gradient flow) form a perfect symmetry, revealing the mathematical elegance of neural network computation
  4. Error-driven learning: error is not an obstacle but the engine of learning; backpropagation transforms error into precise guidance for parameter adjustment
  5. Practical optimization wisdom: learning rate selection, local minimum avoidance, random initialization — these techniques embody the balance between engineering wisdom and theoretical insight in machine learning

Code Practice: Implementing Backpropagation in Python

"Let's use Python code to practice backpropagation," said Mr. Pallas's Cat. "Code not only helps us understand abstract mathematical formulas, but also lets us 'run' this error-driven learning process."

Single-Layer Network Backpropagation

python
import numpy as np
import matplotlib.pyplot as plt

class SingleLayerNetwork:
    """Single-layer neural network (logistic regression), demonstrating backpropagation
    
    Parameters:
        num_features: number of input features
    """
    
    def __init__(self, num_features):
        # Randomly initialize weights and bias
        self.weights = np.random.randn(num_features) * 0.01
        self.bias = 0.0
        self.learning_rate = 0.1
        
    def sigmoid(self, z):
        """Sigmoid activation function"""
        return 1 / (1 + np.exp(-z))
    
    def sigmoid_derivative(self, z):
        """Derivative of sigmoid function"""
        s = self.sigmoid(z)
        return s * (1 - s)
    
    def forward(self, X):
        """Forward pass
        
        Parameters:
            X: input data (n_samples, n_features)
            
        Returns:
            A: activation values
            Z: weighted sum (cached for backpropagation)
        """
        # Weighted sum: Z = XW + b
        Z = np.dot(X, self.weights) + self.bias
        
        # Activation function
        A = self.sigmoid(Z)
        
        return A, Z
    
    def compute_loss(self, y_true, y_pred):
        """Compute binary cross-entropy loss
        
        Parameters:
            y_true: true labels (0 or 1)
            y_pred: predicted probabilities (between 0 and 1)
            
        Returns:
            loss value
        """
        # Avoid log(0)
        epsilon = 1e-15
        y_pred = np.clip(y_pred, epsilon, 1 - epsilon)
        
        # Binary cross-entropy loss
        loss = -np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred))
        return loss
    
    def backward(self, X, y_true, y_pred, Z):
        """Backward pass: compute gradients
        
        Parameters:
            X: input data
            y_true: true labels
            y_pred: predicted probabilities
            Z: weighted sum from forward pass (cached)
            
        Returns:
            dW: gradient of weights
            db: gradient of bias
        """
        m = X.shape[0]  # number of samples
        
        # Output layer error: dL/dA
        dA = -(y_true / y_pred - (1 - y_true) / (1 - y_pred)) / m
        
        # Through activation function gradient: dA/dZ = σ'(Z)
        dZ = dA * self.sigmoid_derivative(Z)
        
        # Compute gradients for weights and bias
        dW = np.dot(X.T, dZ)
        db = np.sum(dZ)
        
        return dW, db
    
    def update_parameters(self, dW, db):
        """Update parameters (gradient descent)
        
        Parameters:
            dW: gradient of weights
            db: gradient of bias
        """
        self.weights -= self.learning_rate * dW
        self.bias -= self.learning_rate * db
    
    def train(self, X, y, epochs=1000, verbose=True):
        """Train the network
        
        Parameters:
            X: training data
            y: training labels
            epochs: number of training epochs
            verbose: whether to print training progress
            
        Returns:
            losses: list of loss values per epoch
        """
        losses = []
        
        for epoch in range(epochs):
            # Forward pass
            y_pred, Z = self.forward(X)
            
            # Compute loss
            loss = self.compute_loss(y, y_pred)
            losses.append(loss)
            
            # Backward pass
            dW, db = self.backward(X, y, y_pred, Z)
            
            # Update parameters
            self.update_parameters(dW, db)
            
            # Print loss every 100 epochs
            if verbose and epoch % 100 == 0:
                accuracy = np.mean((y_pred > 0.5) == y)
                print(f"Epoch {epoch}: loss = {loss:.4f}, accuracy = {accuracy:.2%}")
        
        return losses

# Create and train a single-layer network
print("Single-Layer Neural Network Backpropagation Demo:")
print("=" * 60)

# Generate simple binary classification data
np.random.seed(42)
n_samples = 100
n_features = 2

# Class 0: mean [0, 0], Class 1: mean [1, 1]
X0 = np.random.randn(n_samples//2, n_features) * 0.5
X1 = np.random.randn(n_samples//2, n_features) * 0.5 + 1.0

X = np.vstack([X0, X1])
y = np.hstack([np.zeros(n_samples//2), np.ones(n_samples//2)])

print(f"Data shape: X={X.shape}, y={y.shape}")
print(f"Class distribution: Class 0={np.sum(y==0)} samples, Class 1={np.sum(y==1)} samples")

# Create and train the network
network = SingleLayerNetwork(num_features=n_features)
losses = network.train(X, y, epochs=500, verbose=True)

print(f"\nTraining complete!")
print(f"Final weights: {network.weights}")
print(f"Final bias: {network.bias}")

# Test the network
test_inputs = np.array([[0, 0], [1, 1], [0.5, 0.5]])
predictions, _ = network.forward(test_inputs)
print(f"\nTest predictions:")
for i, (x, p) in enumerate(zip(test_inputs, predictions)):
    print(f"  Input {x} → predicted probability {p:.3f} → class {1 if p>0.5 else 0}")

Visualizing the Backpropagation Process

python
def visualize_backpropagation_process():
    """Visualize the key steps of backpropagation"""
    # Create a simple network for visualization
    viz_network = SingleLayerNetwork(num_features=2)
    viz_network.weights = np.array([0.5, -0.3])
    viz_network.bias = 0.2
    
    # Create a test sample
    test_X = np.array([[0.7, 0.3]])
    test_y = np.array([1])  # true label is 1
    
    print("\nBackpropagation Step-by-Step Visualization:")
    print("=" * 60)
    
    # Step 1: Forward pass
    y_pred, Z = viz_network.forward(test_X)
    print(f"1. Forward pass:")
    print(f"   Input: {test_X[0]}")
    print(f"   Weights: {viz_network.weights}")
    print(f"   Bias: {viz_network.bias}")
    print(f"   Weighted sum Z = {Z[0]:.3f}")
    print(f"   Predicted probability σ(Z) = {y_pred[0]:.3f}")
    
    # Step 2: Compute loss
    loss = viz_network.compute_loss(test_y, y_pred)
    print(f"\n2. Compute loss:")
    print(f"   True label: {test_y[0]}")
    print(f"   Predicted probability: {y_pred[0]:.3f}")
    print(f"   Binary cross-entropy loss: {loss:.4f}")
    
    # Step 3: Backward pass
    dW, db = viz_network.backward(test_X, test_y, y_pred, Z)
    print(f"\n3. Backward pass (compute gradients):")
    print(f"   Output error dL/dA = {-(test_y[0]/y_pred[0] - (1-test_y[0])/(1-y_pred[0])):.3f}")
    print(f"   Sigmoid derivative σ'(Z) = {viz_network.sigmoid_derivative(Z[0]):.3f}")
    print(f"   Gradient dZ = dL/dA * σ'(Z) = {dW[0]/test_X[0,0]:.3f}")
    print(f"   Weight gradient dW = [{dW[0]:.3f}, {dW[1]:.3f}]")
    print(f"   Bias gradient db = {db:.3f}")
    
    # Step 4: Parameter update
    old_weights = viz_network.weights.copy()
    old_bias = viz_network.bias
    viz_network.update_parameters(dW, db)
    
    print(f"\n4. Parameter update (learning rate = {viz_network.learning_rate}):")
    print(f"   Old weights: {old_weights}")
    print(f"   New weights: {viz_network.weights}")
    print(f"   Weight change: {viz_network.weights - old_weights}")
    print(f"   Old bias: {old_bias:.3f}")
    print(f"   New bias: {viz_network.bias:.3f}")
    print(f"   Bias change: {viz_network.bias - old_bias:.3f}")
    
    # Verify that loss decreased after update
    new_pred, _ = viz_network.forward(test_X)
    new_loss = viz_network.compute_loss(test_y, new_pred)
    print(f"\n5. Verify update effect:")
    print(f"   Loss before update: {loss:.4f}")
    print(f"   Loss after update: {new_loss:.4f}")
    print(f"   Loss decrease: {loss - new_loss:.4f}")
    print(f"   Prediction before update: {y_pred[0]:.3f}")
    print(f"   Prediction after update: {new_pred[0]:.3f}")
    print(f"   Closer to true label {test_y[0]}? {abs(new_pred[0]-test_y[0]) < abs(y_pred[0]-test_y[0])}")

# Run visualization
visualize_backpropagation_process()

Multi-Layer Network Backpropagation

python
class TwoLayerNetwork:
    """Two-layer neural network, demonstrating backpropagation in deep networks
    
    Parameters:
        input_size: input layer size
        hidden_size: hidden layer size
        output_size: output layer size
    """
    
    def __init__(self, input_size, hidden_size, output_size):
        # Initialize parameters
        self.W1 = np.random.randn(input_size, hidden_size) * 0.01
        self.b1 = np.zeros((1, hidden_size))
        self.W2 = np.random.randn(hidden_size, output_size) * 0.01
        self.b2 = np.zeros((1, output_size))
        
        self.learning_rate = 0.1
    
    def relu(self, Z):
        """ReLU activation function"""
        return np.maximum(0, Z)
    
    def relu_derivative(self, Z):
        """Derivative of ReLU function"""
        return (Z > 0).astype(float)
    
    def softmax(self, Z):
        """Softmax function (for multi-class classification)"""
        exp_Z = np.exp(Z - np.max(Z, axis=1, keepdims=True))
        return exp_Z / np.sum(exp_Z, axis=1, keepdims=True)
    
    def forward(self, X):
        """Forward pass
        
        Returns:
            cache: cached intermediate results for backpropagation
        """
        # First layer
        Z1 = np.dot(X, self.W1) + self.b1
        A1 = self.relu(Z1)
        
        # Second layer
        Z2 = np.dot(A1, self.W2) + self.b2
        A2 = self.softmax(Z2)
        
        # Cache intermediate results
        cache = {
            'X': X,
            'Z1': Z1, 'A1': A1,
            'Z2': Z2, 'A2': A2
        }
        
        return A2, cache
    
    def compute_loss(self, Y, A2):
        """Compute cross-entropy loss"""
        m = Y.shape[0]
        log_probs = np.log(A2 + 1e-15)  # avoid log(0)
        loss = -np.sum(Y * log_probs) / m
        return loss
    
    def backward(self, Y, cache):
        """Backward pass
        
        Parameters:
            Y: true labels (one-hot encoded)
            cache: cached values from forward pass
            
        Returns:
            grads: gradients for each parameter
        """
        m = Y.shape[0]
        
        # Retrieve intermediate results from cache
        X, Z1, A1, Z2, A2 = cache['X'], cache['Z1'], cache['A1'], cache['Z2'], cache['A2']
        
        # Output layer gradients
        dZ2 = A2 - Y  # elegant derivative of softmax + cross-entropy
        dW2 = np.dot(A1.T, dZ2) / m
        db2 = np.sum(dZ2, axis=0, keepdims=True) / m
        
        # Hidden layer gradients
        dA1 = np.dot(dZ2, self.W2.T)
        dZ1 = dA1 * self.relu_derivative(Z1)
        dW1 = np.dot(X.T, dZ1) / m
        db1 = np.sum(dZ1, axis=0, keepdims=True) / m
        
        grads = {
            'dW1': dW1, 'db1': db1,
            'dW2': dW2, 'db2': db2
        }
        
        return grads
    
    def update_parameters(self, grads):
        """Update parameters"""
        self.W1 -= self.learning_rate * grads['dW1']
        self.b1 -= self.learning_rate * grads['db1']
        self.W2 -= self.learning_rate * grads['dW2']
        self.b2 -= self.learning_rate * grads['db2']
    
    def train(self, X, Y, epochs=1000):
        """Train the network"""
        losses = []
        
        for epoch in range(epochs):
            # Forward pass
            A2, cache = self.forward(X)
            
            # Compute loss
            loss = self.compute_loss(Y, A2)
            losses.append(loss)
            
            # Backward pass
            grads = self.backward(Y, cache)
            
            # Update parameters
            self.update_parameters(grads)
            
            if epoch % 200 == 0:
                predictions = np.argmax(A2, axis=1)
                labels = np.argmax(Y, axis=1)
                accuracy = np.mean(predictions == labels)
                print(f"Epoch {epoch}: loss = {loss:.4f}, accuracy = {accuracy:.2%}")
        
        return losses

# Multi-layer network demo
print("\nTwo-Layer Neural Network Backpropagation Demo:")
print("=" * 60)

# Generate a simple spiral dataset
np.random.seed(42)
n_samples = 200
n_classes = 3

# Create spiral data
theta = np.linspace(0, 4*np.pi, n_samples//n_classes)
radius = np.linspace(0.5, 2.5, n_samples//n_classes)

X_spiral = []
Y_spiral = []

for k in range(n_classes):
    angle = theta + k * 2*np.pi/n_classes
    r = radius + np.random.randn(len(radius))*0.1
    
    x1 = r * np.sin(angle)
    x2 = r * np.cos(angle)
    
    X_spiral.append(np.column_stack([x1, x2]))
    Y_spiral.append(np.full(len(x1), k))

X_spiral = np.vstack(X_spiral)
Y_spiral = np.hstack(Y_spiral)

# Shuffle the data
indices = np.random.permutation(n_samples)
X_spiral = X_spiral[indices]
Y_spiral = Y_spiral[indices]

# One-hot encode labels
Y_onehot = np.eye(n_classes)[Y_spiral]

print(f"Spiral dataset: samples={n_samples}, classes={n_classes}")
print(f"Input shape: {X_spiral.shape}, Output shape: {Y_onehot.shape}")

# Create and train two-layer network
two_layer_net = TwoLayerNetwork(input_size=2, hidden_size=10, output_size=3)
losses_2layer = two_layer_net.train(X_spiral, Y_onehot, epochs=1000)

print(f"\nTraining complete!")

# Visualize decision boundary
def plot_decision_boundary(model, X, y):
    """Plot the neural network's decision boundary"""
    h = 0.02  # grid step size
    x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
    y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
    
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
                         np.arange(y_min, y_max, h))
    
    # Predict class for each point on the grid
    Z, _ = model.forward(np.c_[xx.ravel(), yy.ravel()])
    Z = np.argmax(Z, axis=1)
    Z = Z.reshape(xx.shape)
    
    # Plot decision boundary
    plt.figure(figsize=(10, 8))
    plt.contourf(xx, yy, Z, alpha=0.3, cmap='RdBu')
    
    # Plot data points
    scatter = plt.scatter(X[:, 0], X[:, 1], c=y, cmap='RdBu', 
                         edgecolors='black', s=50, alpha=0.8)
    
    plt.xlabel('Feature x₁')
    plt.ylabel('Feature x₂')
    plt.title('Two-Layer Neural Network Decision Boundary (Spiral Data)')
    plt.colorbar(scatter, label='Class')
    plt.grid(True, alpha=0.3)
    
    plt.savefig('/tmp/two_layer_decision_boundary.png', dpi=150, bbox_inches='tight')
    plt.close()
    
    print("Decision boundary visualization saved to /tmp/two_layer_decision_boundary.png")

plot_decision_boundary(two_layer_net, X_spiral, Y_spiral)

Mental Model: Optimization Variants of Backpropagation

python
def compare_optimization_algorithms():
    """Compare different optimization algorithms"""
    # Generate simple quadratic loss function: L(w) = (w - 2)^2
    def loss_function(w):
        return (w - 2) ** 2
    
    def gradient(w):
        return 2 * (w - 2)
    
    # Different optimization algorithms
    def gradient_descent(w_init, lr=0.1, epochs=50):
        """Standard gradient descent"""
        w = w_init
        history = [w]
        
        for _ in range(epochs):
            w = w - lr * gradient(w)
            history.append(w)
        
        return np.array(history)
    
    def momentum_gd(w_init, lr=0.1, beta=0.9, epochs=50):
        """Gradient descent with momentum"""
        w = w_init
        v = 0  # momentum term
        history = [w]
        
        for _ in range(epochs):
            g = gradient(w)
            v = beta * v + (1 - beta) * g
            w = w - lr * v
            history.append(w)
        
        return np.array(history)
    
    def rmsprop(w_init, lr=0.1, beta=0.9, epsilon=1e-8, epochs=50):
        """RMSProp optimizer"""
        w = w_init
        s = 0  # moving average of squared gradients
        history = [w]
        
        for _ in range(epochs):
            g = gradient(w)
            s = beta * s + (1 - beta) * g**2
            w = w - lr * g / (np.sqrt(s) + epsilon)
            history.append(w)
        
        return np.array(history)
    
    # Compare different optimizers
    print("\nOptimization Algorithm Comparison (target: minimize L(w) = (w - 2)²):")
    print("=" * 60)
    
    w_init = -3.0  # initial point
    target = 2.0   # optimal value
    
    # Run different optimizers
    gd_path = gradient_descent(w_init, lr=0.3, epochs=30)
    momentum_path = momentum_gd(w_init, lr=0.3, epochs=30)
    rmsprop_path = rmsprop(w_init, lr=0.3, epochs=30)
    
    # Compute final errors
    gd_error = abs(gd_path[-1] - target)
    momentum_error = abs(momentum_path[-1] - target)
    rmsprop_error = abs(rmsprop_path[-1] - target)
    
    print(f"Initial value: w = {w_init:.2f}")
    print(f"Target value: w* = {target:.2f}")
    print(f"\nFinal results:")
    print(f"  Standard GD: w = {gd_path[-1]:.4f}, error = {gd_error:.4f}")
    print(f"  Momentum GD: w = {momentum_path[-1]:.4f}, error = {momentum_error:.4f}")
    print(f"  RMSProp:      w = {rmsprop_path[-1]:.4f}, error = {rmsprop_error:.4f}")
    
    # Visualize optimization paths
    plt.figure(figsize=(12, 5))
    
    # Loss function curve
    w_range = np.linspace(-3, 4, 100)
    loss_range = loss_function(w_range)
    
    plt.subplot(1, 2, 1)
    plt.plot(w_range, loss_range, 'b-', linewidth=2, label='L(w) = (w-2)²')
    plt.plot(gd_path, loss_function(gd_path), 'ro-', label='Standard GD', alpha=0.6)
    plt.plot(momentum_path, loss_function(momentum_path), 'gs-', label='Momentum GD', alpha=0.6)
    plt.plot(rmsprop_path, loss_function(rmsprop_path), 'm^-', label='RMSProp', alpha=0.6)
    
    plt.xlabel('Parameter w')
    plt.ylabel('Loss L(w)')
    plt.title('Optimization Algorithm Paths on Loss Function')
    plt.legend()
    plt.grid(True, alpha=0.3)
    
    # Parameter convergence curves
    plt.subplot(1, 2, 2)
    plt.plot(range(len(gd_path)), gd_path, 'ro-', label='Standard GD', alpha=0.6)
    plt.plot(range(len(momentum_path)), momentum_path, 'gs-', label='Momentum GD', alpha=0.6)
    plt.plot(range(len(rmsprop_path)), rmsprop_path, 'm^-', label='RMSProp', alpha=0.6)
    plt.axhline(y=target, color='black', linestyle='--', label='Target value')
    
    plt.xlabel('Iteration')
    plt.ylabel('Parameter w')
    plt.title('Parameter Convergence over Iterations')
    plt.legend()
    plt.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.savefig('/tmp/optimization_algorithms_comparison.png', dpi=150, bbox_inches='tight')
    plt.close()
    
    print("\nOptimization algorithm comparison chart saved to /tmp/optimization_algorithms_comparison.png")
    
    # Analyze characteristics of different algorithms
    print("\nOptimization Algorithm Characteristics Analysis:")
    print("  Standard Gradient Descent:")
    print("    - Simple and direct; updates along negative gradient direction each step")
    print("    - Learning rate selection is critical: too large may cause oscillation, too small converges slowly")
    print("    - May zigzag in narrow valleys")
    
    print("\n  Momentum Gradient Descent:")
    print("    - Introduces the 'momentum' concept, accumulating previous gradient directions")
    print("    - Reduces oscillation, accelerates convergence in valleys")
    print("    - Hyperparameter β controls the weight of historical gradients")
    
    print("\n  RMSProp:")
    print("    - Adaptively adjusts per-parameter learning rates")
    print("    - Uses smaller learning rates for frequently updated parameters")
    print("    - Uses larger learning rates for sparse parameters")
    
    print("\n  Practical recommendations:")
    print("    - Simple problems: standard GD or momentum GD")
    print("    - Complex problems: Adam (combines momentum and RMSProp)")
    print("    - Learning rate: typically start from 0.001 or 0.0001")

# Run optimization algorithm comparison
compare_optimization_algorithms()

"Remember," Mr. Pallas's Cat summarized, "backpropagation is the key algorithm of deep learning — it transforms error into guidance for learning. Through gradient descent, the network slowly descends the slope of the loss function; through the chain rule, error propagates backward from the output layer to every layer. This process is not only efficient but elegant — it shows how mathematics transforms 'knowing the error' into 'knowing how to correct it.' Most importantly, backpropagation teaches us wisdom about learning itself: progress is not a straight line forward, but a zigzag ascent through trial and error."


Mr. Pallas's Cat's Reflection Questions

Hands-On Exploration (for Piglet)

  1. Gradient experiment: modify the code above and try different learning rates (0.001, 0.01, 0.1, 1.0). Observe how the training process differs. When does it diverge?
  2. Activation function comparison: change sigmoid to ReLU or tanh; observe how backpropagation computations change. How does the vanishing gradient problem manifest?
  3. Network depth experiment: create a three-layer network and observe how gradients propagate in deep networks. Why are deeper networks harder to train?

Historical Investigation (for Little Seal)

  1. Algorithm origins: research the history of the backpropagation algorithm. Who first proposed this idea? Why wasn't it widely accepted until the 1980s?
  2. The deep learning revival: investigate the key factors behind the deep learning revival around 2010. What roles did hardware (GPUs), data (ImageNet), and algorithms (ReLU, Dropout) each play?
  3. Neuroscience connection: what are the similarities and differences between backpropagation and the brain's learning mechanisms? Is there evidence that the brain uses something similar to backpropagation?

Integrated Reflection

  1. Philosophical reflection: gradient descent is a "local optimization" strategy — always descending along the steepest slope. What parallels does this have with life decisions? When might this strategy fail?
  2. Ethical challenge: when neural networks "learn" through backpropagation, they may amplify biases present in training data. How can we detect and mitigate this bias amplification?
  3. Creative exercise: design a "meta-learning" system that learns how to learn better (i.e., optimizes its own learning rate). How would you design it?
  4. Ultimate challenge: prove that gradient descent converges to a local minimum (under appropriate conditions). What mathematical assumptions are required?

Coming Up Next

The fragrance of tea filled the Black Stone House; night deepened.

"Today we explored the wisdom of backpropagation," said Mr. Pallas's Cat. "A single neuron learned to adjust itself from its errors. But true memory requires time — how does a network remember past information?"

Piglet asked curiously: "Memory? Like how we remember what we just said?"

"Yes," Mr. Pallas's Cat explained. "In the next chapter, we'll explore chains of memory. When information flows through time, how does a network maintain state? This is the story of recurrent neural networks (RNNs) and long short-term memory (LSTMs)."

Little Seal flipped through his notebook. "This introduces the history of sequence modeling. Historically, how did people handle time-series data?"

Mr. Pallas's Cat smiled. "We'll take it slow. See you in the next chapter."


Piglet's note: I implemented a two-layer network and trained it to classify spiral data. Accuracy started at 33% (random guessing) and reached 92% after 1000 epochs! But I also found that if the learning rate is too high (0.5), the loss explodes; if too low (0.001), it barely learns. Finding the right step size is like finding the right rhythm for descending a mountain — neither too hurried nor too slow.

Little Seal's note: I researched the history of backpropagation and was surprised that it went through a cycle of "invention – forgetting – rediscovery." Similar ideas existed in the 1960s, but it wasn't until Rumelhart et al.'s 1986 paper that widespread attention was drawn. Most fascinatingly, the mathematical foundation (the chain rule) had long existed in calculus, but applying it to neural networks required cross-disciplinary insight.

Mr. Pallas's Cat's closing words: Backpropagation teaches a profound lesson about learning: progress requires feedback, optimization requires direction, complexity requires decomposition. In this algorithm, we see the union of mathematical elegance and practical wisdom. Most importantly, it reminds us: error is not the endpoint, but the starting point for beginning again. On this path, patience matters more than speed, and direction more than force. We'll take it slow — understanding is what matters most.