Designing Machine Learning ApplicationsDesigning Machine Learning Applications
Home
Discus
Home
Discus
  • Contents
  • Preface

    • About the Author
    • About DMLA
  • Mathematical Foundations

    • Linear Algebra

      • Vector Basics
      • Matrix Basics
    • Calculus

      • Limits, Derivatives, and Differentials
      • Multivariate Functions and Composite Function Differentiation
    • Statistics and Probability

      • Probability Basics
      • Statistical Inference
  • Classical Statistical Learning

    • Linear Models

      • Linear Regression
      • Logistic Regression
      • Regularization and Generalized Linear Models
    • Bayesian Methods

      • Naive Bayes
      • Bayesian Network
      • EM Algorithm
    • Support Vector Machines

      • Support Vector Machine
      • Kernel Trick
    • Decision Trees and Ensembles

      • Decision Trees
      • Random Forest
      • Boosting
    • Unsupervised Learning

      • Clustering
      • Dimensionality Reduction
  • Neural Networks and Deep Learning

    • Neural Network Architectures

      • Fundamentals of Neural Networks
      • Linear Perceptron
      • Multi-Layer Perceptron
      • Forward Propagation
      • Backpropagation
      • Activation Functions and Loss Functions
    • Optimization

      • Gradient Descent
      • Adaptive Optimizers
    • Deep Network Stability

      • Weight Initialization
      • Dropout Regularization
      • Batch Normalization
    • Convolutional Neural Networks

      • CNN Basics
      • AlexNet and the CNN Revival
      • VGG and GoogLeNet
      • ResNet Residual Network
      • Lab: AlexNet Image Classification
    • Generative Models

      • Variational Autoencoder
      • Generative Adversarial Network
      • Lab: DCGAN Image Generation
    • Sequence Models

      • Word Embedding and Representation Learning
      • RNN Fundamentals
      • LSTM and GRU Gating Mechanisms
      • Seq2Seq Sequence Mapping
      • Lab: LSTM Poetry Generation
  • The Language Model Singularity

    • Transformer Architecture

      • Transformer Fundamentals
      • Transformer Evolution and Variants
      • Language Models and Tokenization
      • Lab: Transformer Model Training
    • Pretraining and Fine-Tuning

      • Pretraining Data Engineering
      • Scaling Laws
      • Distributed Training Infrastructure
      • Supervised Fine-Tuning
      • Lab: SFT Model Conversation
    • Alignment Training

      • Reinforcement Learning from Human Feedback
      • Evolution of Alignment Methods
      • Lab: DPO Alignment Training
    • Reasoning Capabilities

      • Chain of Thought and Reasoning Models
      • Test-Time Compute Scaling
      • Inference Efficiency Optimization
      • Lab: LLM Inference Optimization
    • Multimodal Fusion and Safety

      • Multimodal Large Language Models
      • Model Evaluation and Safety
      • Lab: VLM Training
  • AI Infrastructure and Engineering

    • Model Serving

      • Inference Service Architecture
      • Request Scheduling and Batching
      • GPU Resource Management
      • Lab: Deploying LLM Inference Service
    • MLOps Practices

      • Data Versioning
      • Experiment Tracking and Model Registry
      • Hyperparameter Optimization
      • Model Performance Monitoring
      • Drift Detection
  • Agentic Application Systems

    • Vector Retrieval and RAG

      • Embedding and Vector Retrieval
      • Retrieval Quality Evaluation and Optimization
      • Retrieval-Augmented Generation
      • Lab: Building a Knowledge Base Q&A System
    • Building Agent Applications

      • From LLM to Agent
      • Tool Use
      • Planning and Reasoning
      • Memory Systems
      • Agent Collaboration and Communication
      • Orchestration and Fault Tolerance
      • Lab: Research Agent Collaboration System
  • Appendix

    • Building the Sandbox Environment
    • NumPy Practice

      • Data Processing Practice
      • Calculus Computation Practice
      • Probability and Statistics Practice

Forward Propagation

In the previous three chapters, we gradually built the theoretical framework of neural networks. From biological neurons to the M-P model, from single-layer perceptrons to multi-layer perceptrons, from linear decision boundaries to nonlinear representation capabilities. Now, we will delve into the computational mechanism of neural networks — Forward Propagation. The concept of forward propagation can be traced back to 1943. When McCulloch and Pitts proposed the M-P model, the process they described of neurons receiving input signals, performing weighted summation, thresholding, and outputting signals was the prototype of forward propagation. Today, forward propagation specifically refers to the process in neural networks where signals are transmitted layer by layer from the input layer through each layer of neurons to the output layer. This chapter will introduce the signal flow process, matrix form derivation, the concept of computational graphs, batch computation and efficiency optimization, and experimentally compare the speed differences of forward propagation on CPU and GPU.

Signal Flow Process

When introducing perceptrons and multi-layer perceptrons earlier, we examined signal flow from the perspective of a single neuron: a neuron receives multiple input signals x=(x1,x2,…,xn)T\mathbf{x} = (x_1, x_2, \ldots, x_n)^Tx=(x1​,x2​,…,xn​)T, multiplies each input signal by its corresponding weight and adds a bias to obtain the linear combination result z=∑i=1nwixi+b=wTx+bz = \sum_{i=1}^{n} w_i x_i + b = \mathbf{w}^T \mathbf{x} + bz=∑i=1n​wi​xi​+b=wTx+b, and then transforms it through an activation function fff to produce the neuron output a=f(z)a = f(z)a=f(z). This three-step process of "input → weighted sum → activation transformation" constitutes the basic computation of a neuron.

When we organize multiple neurons into layers and connect them into a network, we must use matrix operations to describe the process of input signals passing through multiple neurons layer by layer from the perspective of the entire network. Suppose we have an LLL-layer neural network (including input layer, hidden layers, and output layer). The weight matrix of the lll-th layer is denoted as Wl\mathbf{W}^lWl, the bias vector as bl\mathbf{b}^lbl. It receives the output al−1\mathbf{a}^{l-1}al−1 of the (l−1)(l-1)(l−1)-th layer and performs a linear combination zl=Wlal−1+bl\mathbf{z}^l = \mathbf{W}^l \mathbf{a}^{l-1} + \mathbf{b}^lzl=Wlal−1+bl, where the dimension of the weight matrix Wl\mathbf{W}^lWl is nl×nl−1n_l \times n_{l-1}nl​×nl−1​ (the number of rows equals the number of neurons in the current layer, and the number of columns equals the number of neurons in the previous layer), and the dimension of the bias vector bl\mathbf{b}^lbl is nl×1n_l \times 1nl​×1. The result is then transformed through the activation function flf^lfl to obtain the output of this layer al=fl(zl)\mathbf{a}^l = f^l(\mathbf{z}^l)al=fl(zl). This process is repeated for each layer until the output aL\mathbf{a}^LaL of the last layer is the network's prediction for the input x\mathbf{x}x. If you understand the signal flow of a sample through a single neuron, then the signal flow through the entire network is simply a generalization where parameters become matrices. This process can be intuitively represented with a flowchart:

Figure: Information flow process in a multi-layer neural network

From the above diagram, we can see that signal flow is a relay process: the input signal enters the first layer, undergoes linear combination and activation transformation, and the output serves as the input for the second layer, and so on, until the output layer produces the final prediction. Each layer of the network processes the signal transmitted from the previous layer — some layers extract features, some combine features, and some make decisions.

Now we have generalized the signal flow from a single neuron to the entire network, but this is still only the computation process of a single sample entering the network. In practical applications, neural networks typically need to process thousands or tens of thousands of samples. Image recognition needs to process massive numbers of images, speech recognition needs to process large amounts of audio clips. If we compute forward propagation sample by sample, the efficiency would be far too low. So we need to go further and provide the matrix form for batch input, completing batch computations in one pass.

Suppose the input contains mmm samples, each with n0n_0n0​ features, the input matrix X∈Rn0×m\mathbf{X} \in \mathbb{R}^{n_0 \times m}X∈Rn0​×m, where each column is a sample vector X=[x1,x2,…,xm]\mathbf{X} = [\mathbf{x}_1, \mathbf{x}_2, \ldots, \mathbf{x}_m]X=[x1​,x2​,…,xm​]. The lll-th layer of an LLL-layer network receives the batch output Al−1∈Rnl−1×m\mathbf{A}^{l-1} \in \mathbb{R}^{n_{l-1} \times m}Al−1∈Rnl−1​×m of the (l−1)(l-1)(l−1)-th layer, then performs a linear combination Zl=WlAl−1+bl\mathbf{Z}^l = \mathbf{W}^l \mathbf{A}^{l-1} + \mathbf{b}^lZl=WlAl−1+bl, and then transforms it through the activation function flf^lfl to obtain the output matrix Al=fl(Zl)\mathbf{A}^l = f^l(\mathbf{Z}^l)Al=fl(Zl) of the lll-th layer. If you understand the signal flow of a single sample through the network, then the signal flow for batch samples is simply a generalization where variables become matrices. This process can also be intuitively represented with a flowchart:

Figure: Information flow process of batch samples in a multi-layer neural network

As can be seen from the diagram, the signal flow process during batch processing is exactly the same as for a single sample, except that variables are extended from vectors to matrices. Each column of the input matrix X\mathbf{X}X represents one sample. After linear combination and activation transformation through each layer, each column of the output matrix AL\mathbf{A}^LAL corresponds to the prediction result for that sample. The matrix operation completes the computation for all mmm samples at once, fully leveraging the parallel computing capability of modern hardware.

However, there is a minor flaw in the computation that needs to be addressed. Attentive readers may notice a detail issue in the formula Zl=WlAl−1+bl\mathbf{Z}^l = \mathbf{W}^l \mathbf{A}^{l-1} + \mathbf{b}^lZl=WlAl−1+bl: the result of WlAl−1\mathbf{W}^l \mathbf{A}^{l-1}WlAl−1 has size nl×mn_l \times mnl​×m, while bl\mathbf{b}^lbl has size nl×1n_l \times 1nl​×1. The shapes do not match, so they cannot be added directly. This is resolved through the Broadcasting Mechanism. In frameworks like NumPy and PyTorch, the bias vector of shape nl×1n_l \times 1nl​×1 is automatically expanded to nl×mn_l \times mnl​×m, with each column being the same bias vector. This is equivalent to adding the same bias to all samples, because the bias is an inherent parameter of the neuron and does not vary with the sample.

At this point, we first expanded parameters into matrix form, generalizing from single-neuron computation to the entire network, and then expanded variables into matrix form, generalizing from single-sample computation to batch processing. The computation process expressed in terms of parameters and variables is represented as a combination of matrix multiplication and nonlinear transformations, which facilitates both the derivation of backpropagation formulas and the utilization of matrix operation optimization capabilities of hardware such as GPUs. The underlying implementations of modern deep learning frameworks (TensorFlow, PyTorch) are all based on matrix operations.

Computational Graph

The matrix form expresses forward propagation as compact mathematical formulas. Now let us shift our focus from mathematical theory to engineering practice, exploring how deep learning frameworks manage complex computation flows. The key to modern deep learning frameworks efficiently handling the vast computational workload of neural networks is the Computational Graph. It is not only a powerful tool for understanding and debugging neural networks but also the foundation of Automatic Differentiation, providing technical support for backpropagation.

A computational graph is a graphical method for representing computation processes. In the graph, nodes represent operations (such as addition, multiplication, activation functions), and edges represent data flow, decomposing complex computation processes into combinations of basic operations. The forward propagation of a neural network can be naturally represented as a computational graph. Each neuron contains two nodes: a linear combination node (z=Wx+b\mathbf{z} = \mathbf{W}\mathbf{x} + \mathbf{b}z=Wx+b) and an activation node (a=f(z)\mathbf{a} = f(\mathbf{z})a=f(z)). Multi-layer networks connect these nodes by layers to form a complete computational graph. Taking a single-layer perceptron as an example, the computational graph structure is as follows:

Figure: Single-layer perceptron computational graph

Each node in the diagram represents an operation: weight multiplication (W⋅x\mathbf{W}·\mathbf{x}W⋅x), bias addition (zzz), and activation transformation (aaa). Data flows along edges from input nodes to output nodes, forming a complete computation chain. For multi-layer networks, the computational graph is a series connection of multiple single-layer computational graphs, as shown in the following diagram:

Figure: Multi-layer network computational graph

In modern computing systems, computational graphs offer many conveniences and advantages, including:

  1. Visualizing the computation process: Computational graphs intuitively show how data flows from input to output and what operations are performed at each step. When debugging neural networks, visualizing the computational graph can help locate computation errors and understand intermediate results.
  2. Supporting automatic differentiation: This is the most important advantage of computational graphs. Backpropagation requires computing gradients, and manually deriving gradient formulas is tedious and error-prone. Computational graphs are the foundation of automatic differentiation — backpropagation traverses backward along the computational graph, automatically computing the gradient at each node. This is the underlying mechanism by which frameworks like TensorFlow and PyTorch can automatically compute gradients.
  3. Facilitating modular implementation: Computational graphs decompose computation into basic operational units (addition, multiplication, activation functions, etc.), making modular implementation and combination easy. Each operational unit is an independent building block that can be freely combined to construct complex network structures. Current deep learning frameworks are all built on computational graphs and provide rich libraries of operational units.
  4. Supporting optimization: The structure of computational graphs makes it easy to analyze computation dependencies and perform optimization. For example, operator fusion merges multiple consecutive operations into a single operation, reducing memory access times; parallel computation identifies computation nodes without dependencies and executes them in parallel to improve efficiency. These optimizations have been widely implemented in modern frameworks.

Static Graph vs Dynamic Graph

In deep learning frameworks, there are two ways to build computational graphs: static graphs and dynamic graphs. Understanding the difference between the two helps in understanding the design philosophies of different frameworks (TensorFlow vs PyTorch).

  • Static Graph: The complete computational graph structure is defined first, and then computation is executed. Early versions of TensorFlow (1.x) adopted this approach. Users first "draw" the blueprint of the computational graph, then the framework optimizes the blueprint (operator fusion, memory planning, etc.), and finally executes the optimized computational graph. The advantage is that the computational graph can be optimized in advance, resulting in high execution efficiency; the disadvantage is low flexibility, making it difficult to handle dynamic structures (such as conditional branches, variable-length sequences), and hard to inspect intermediate results in real-time during debugging.

  • Dynamic Graph: The computational graph is built dynamically during execution. PyTorch adopts this approach. Each time forward propagation is executed, the framework builds the computational graph on the fly as it computes. The advantage is high flexibility — Python's conditional branches, loops, and other control flows can be freely used, making debugging easy (you can directly print intermediate values, set breakpoints); the disadvantage is that no pre-optimization is possible, and the graph must be rebuilt for each execution.

To use a real-world analogy, a static graph is like a "pre-made meal" — the factory designs the recipe and prepares the ingredients in advance, and the user just follows the procedure to execute; it is efficient but not customizable. A dynamic graph is like "cooking on the spot" — the chef adjusts in real-time according to the actual situation; it is flexible but relatively less computationally efficient. Modern frameworks are gradually incorporating the advantages of both approaches. TensorFlow 2.x supports dynamic graph mode (Eager Execution), but can convert to static graphs for optimization via tf.function; PyTorch defaults to dynamic graphs but supports JIT compilation to optimize dynamic graphs into static graphs. This fusion allows users to enjoy the flexibility of dynamic graphs during development and the efficiency of static graphs during production deployment.

Batch Computation and Efficiency Optimization

The matrix form lays the foundation for Batch Processing. Batch processing is not just a mathematical technique — it is at the core of neural network training efficiency. Neural network training typically involves a large number of samples. For example, in image classification, the MNIST dataset has 60,000 samples, the CIFAR-10 dataset has 50,000 samples, and the ImageNet dataset has 1.28 million samples. In text classification for language models, the number of samples is measured in hundreds of millions. Thus, batch processing is an essential means of optimizing computational efficiency. It merges multiple samples into a single matrix and completes the computation in one pass. Let the batch size be BBB. Forward propagation processes BBB samples at a time and outputs BBB prediction results; backpropagation updates weights based on the average gradient of the BBB samples. The value of batch processing can be understood from the following three perspectives:

  1. Computational Efficiency: Matrix operations can leverage the parallel computing capability of hardware such as GPUs. Modern GPUs have thousands to tens of thousands of computing cores (e.g., the NVIDIA A100 has 6912 CUDA cores) and were designed from the outset for efficient matrix computation execution. Processing BBB samples takes much less time than the sum of processing BBB samples one by one — just as a truck transporting 1000 bricks at once is certainly much faster than a worker carrying 1000 bricks back and forth.
  2. Gradient Stability: The average gradient based on multiple samples is more stable than the gradient from a single sample. The gradient of a single sample may be affected by noise, making the direction unstable; the batch gradient is the average over multiple samples, where noise is smoothed out, the gradient direction is more stable, and training is more steady. As an analogy, the gradient from a single sample is like blind men touching an elephant — each sample only reflects local information; the batch gradient is analogous to observing from multiple angles, synthesizing information from many samples, resulting in a more accurate direction.
  3. Memory Utilization: Batch processing can better utilize memory bandwidth. GPUs compute very fast, but data transfer speed is relatively slow. Batch processing reduces the number of data transfers, keeping GPU compute cores continuously working rather than waiting for data transfer.

Therefore, the batch size BBB is also an important hyperparameter of the neural network model, requiring a trade-off among efficiency, stability, and memory. The following table lists common batch size ranges and selection principles:

Batch Size RangeCharacteristicsSuitable Scenarios
Small batch (B=16B=16B=16-646464)Large gradient noise, training fluctuations, but helps escape local optima; low memory usageMemory-constrained scenarios, research experiments, pursuing generalization performance
Medium batch (B=128B=128B=128-512512512)Balances efficiency and stability, moderate gradient noiseCommon choice, most training scenarios
Large batch (B=1024B=1024B=1024+)High computational efficiency, stable gradients, but may get stuck in local optima; high memory usageLarge-scale training, GPU/TPU high-performance hardware

It is not true that larger batch sizes are always better, as long as memory (VRAM) allows. The large gradient noise of small batches is not entirely a bad thing — the noise is equivalent to adding random perturbations to the gradient direction, which can help escape local optima or saddle points and explore a wider area. Large batch gradients are smooth and stable but lack this random exploration capability, making them prone to converging stably along the gradient direction to local optima. Research has shown that models trained with large batches often perform better on the training set but worse on the test set (large generalization gap), possibly because the noise from small batches forces the model to learn more robust features.

Since the computational efficiency of forward propagation directly affects the speed of training and inference, modern deep learning frameworks have implemented various optimization strategies. Users generally do not need to optimize manually, but understanding these strategies helps in better utilizing the framework's capabilities. The main optimization strategies in modern deep learning frameworks include:

  1. Matrix Operation Optimization: Leveraging GPU acceleration for matrix multiplication (e.g., CUDA, cuBLAS). Computing an n×nn \times nn×n matrix multiplication on CPU takes O(n3)O(n^3)O(n3) time; GPUs can utilize thousands of cores for parallel computation, achieving tens or even hundreds of times speedup.
  2. Operator Fusion: Merging linear combination and activation transformation into a single operation, reducing the storage and transfer of intermediate results. The traditional implementation first computes zl=Wlal−1+bl\mathbf{z}^l = \mathbf{W}^l \mathbf{a}^{l-1} + \mathbf{b}^lzl=Wlal−1+bl, stores zl\mathbf{z}^lzl, and then computes al=f(zl)\mathbf{a}^l = f(\mathbf{z}^l)al=f(zl). The fused implementation directly computes al=f(Wlal−1+bl)\mathbf{a}^l = f(\mathbf{W}^l \mathbf{a}^{l-1} + \mathbf{b}^l)al=f(Wlal−1+bl), eliminating the step of storing zl\mathbf{z}^lzl and reducing memory access. Going further, multiple consecutive operations can be merged into a single composite operation, reducing the number of computational graph nodes and lowering execution overhead. For example, merging the three nodes "matrix multiplication → bias addition → activation function" into a single node reduces the storage and transfer of intermediate results and reduces the number of function calls.
  3. Memory Reuse: Reusing memory space for intermediate results to reduce memory allocation overhead. For example, after Zl\mathbf{Z}^lZl is computed, if Zl\mathbf{Z}^lZl is no longer needed later, Al\mathbf{A}^lAl can reuse the memory of Zl\mathbf{Z}^lZl. This reduces the number of memory allocations and improves execution efficiency.
  4. Mixed Precision Computation: Using low-precision floating-point numbers (such as FP16, half-precision) for computation, reducing memory usage and computation time while maintaining sufficient numerical precision. FP16 uses half the memory of FP32 (single precision) and computes faster. Modern GPUs (such as NVIDIA V100, A100) have specialized optimizations for FP16 computation, achieving speeds several times that of FP32. Mixed precision computation can be used at different precision levels — for example, the main computation flow uses FP16, but gradient accumulation uses FP32 to avoid training instability caused by precision loss.

These optimization strategies have been widely implemented in modern deep learning frameworks (TensorFlow, PyTorch). Users only need to call the operators provided by the framework (such as nn.Linear, nn.ReLU), and the framework automatically applies the optimizations. However, this knowledge is still useful for understanding framework behavior, debugging performance issues, and customizing optimization solutions.

Forward Propagation Algorithm Practice

Through the following code implementation, we can intuitively experience the process of matrix operations, verify the correctness of dimension checks, and understand how signals flow between layers. The experiment below implements forward propagation in a multi-layer neural network, demonstrating the computation process for both single-sample and batch processing scenarios, and visualizes the network structure and computation flow. The experiment first defines a general neural network class, then compares forward propagation speeds between CPU and GPU at different batch sizes, compares performance differences under different network scales, and finally visualizes the GPU acceleration effect through charts, providing an intuitive understanding of the value of GPU parallel computing in deep learning.

From the running results, it can be seen that due to startup costs such as memory-to-VRAM copying, GPU has no efficiency advantage in very small neural networks. However, as the network scale increases, the advantages of GPU quickly become apparent.

import torch
import torch.nn as nn
import time
import matplotlib.pyplot as plt

# Check if GPU is available
device_gpu = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
device_cpu = torch.device('cpu')

print("=" * 60)
print("PyTorch CPU vs GPU Forward Propagation Speed Comparison")
print("=" * 60)
print(f"CPU device: {device_cpu}")
print(f"GPU device: {device_gpu}")
print(f"GPU name: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'N/A'}")
print()


class NeuralNetworkPyTorch(nn.Module):
    """
    Multi-layer neural network PyTorch implementation
    """
    def __init__(self, layer_sizes):
        """
        Parameters:
        layer_sizes : list of int
            Number of neurons in each layer, e.g. [784, 256, 128, 10]
        """
        super(NeuralNetworkPyTorch, self).__init__()
        self.layer_sizes = layer_sizes

        # Build network layers
        layers = []
        for i in range(len(layer_sizes) - 1):
            layers.append(nn.Linear(layer_sizes[i], layer_sizes[i+1]))
            if i < len(layer_sizes) - 2:  # Add ReLU except for the last layer
                layers.append(nn.ReLU())
            else:
                layers.append(nn.Sigmoid())  # Use Sigmoid for output layer

        self.network = nn.Sequential(*layers)

    def forward(self, x):
        return self.network(x)


def benchmark_forward(model, input_data, device, num_iterations=100):
    """
    Benchmark: measure forward propagation time
    """
    model = model.to(device)
    data = input_data.to(device)

    # Warmup
    for _ in range(10):
        _ = model(data)

    if device.type == 'cuda':
        torch.cuda.synchronize()

    # Formal timing
    start_time = time.perf_counter()
    for _ in range(num_iterations):
        _ = model(data)
        if device.type == 'cuda':
            torch.cuda.synchronize()
    end_time = time.perf_counter()

    avg_time = (end_time - start_time) / num_iterations
    return avg_time


# Experiment 1: Speed comparison across different batch sizes
print("Experiment 1: CPU vs GPU Speed Comparison Across Different Batch Sizes")
print("-" * 60)

# Network configuration: Simulating MNIST-scale network
layer_sizes = [784, 512, 256, 128, 10]  # Input 784 (28x28 image), 3 hidden layers, output 10 (classification)
model = NeuralNetworkPyTorch(layer_sizes)
model.eval()  # Evaluation mode

print(f"Network structure: {' -> '.join(map(str, layer_sizes))}")
print(f"Total parameters: {sum(p.numel() for p in model.parameters()):,}")
print()

# Test different batch sizes
batch_sizes = [16, 64, 256, 1024, 4096]
cpu_times = []
gpu_times = []
speedups = []

print(f"{'Batch Size':<12} {'CPU Time(ms)':<15} {'GPU Time(ms)':<15} {'Speedup':<10}")
print("-" * 60)

for bs in batch_sizes:
    # Generate random input data
    input_data = torch.randn(bs, layer_sizes[0])

    # CPU test
    cpu_time = benchmark_forward(model, input_data, device_cpu, num_iterations=50)
    cpu_times.append(cpu_time * 1000)  # Convert to milliseconds

    # GPU test
    if torch.cuda.is_available():
        gpu_time = benchmark_forward(model, input_data, device_gpu, num_iterations=100)
        gpu_times.append(gpu_time * 1000)
        speedup = cpu_time / gpu_time
        speedups.append(speedup)
        print(f"{bs:<12} {cpu_time*1000:<15.3f} {gpu_time*1000:<15.3f} {speedup:<10.1f}x")
    else:
        print(f"{bs:<12} {cpu_time*1000:<15.3f} {'N/A':<15} {'N/A':<10}")


# Experiment 2: Speed comparison across different network scales
print("\n\nExperiment 2: CPU vs GPU Speed Comparison Across Different Network Scales")
print("-" * 60)

# Fixed batch size
fixed_batch_size = 256

# Networks of different scales
network_configs = [
    ('Small network [256, 128, 64, 10]', [256, 128, 64, 10]),
    ('Medium network [784, 256, 128, 10]', [784, 256, 128, 10]),
    ('Large network [1024, 512, 256, 128, 10]', [1024, 512, 256, 128, 10]),
    ('Very large network [4096, 2048, 1024, 512, 10]', [4096, 2048, 1024, 512, 10]),
]

print(f"Fixed batch size: {fixed_batch_size}")
print()
print(f"{'Network Scale':<30} {'CPU Time(ms)':<15} {'GPU Time(ms)':<15} {'Speedup':<10}")
print("-" * 75)

network_speedups = []
network_labels = []

for name, layers in network_configs:
    net = NeuralNetworkPyTorch(layers)
    net.eval()
    input_data = torch.randn(fixed_batch_size, layers[0])

    cpu_time = benchmark_forward(net, input_data, device_cpu, num_iterations=30)

    if torch.cuda.is_available():
        gpu_time = benchmark_forward(net, input_data, device_gpu, num_iterations=100)
        speedup = cpu_time / gpu_time
        network_speedups.append(speedup)
        network_labels.append(name.split('[')[0].strip())
        print(f"{name:<30} {cpu_time*1000:<15.3f} {gpu_time*1000:<15.3f} {speedup:<10.1f}x")
    else:
        print(f"{name:<30} {cpu_time*1000:<15.3f} {'N/A':<15} {'N/A':<10}")


# Experiment 3: Visualize speed comparison results
print("\n\nExperiment 3: Visualize Speed Comparison")
print("-" * 60)

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Figure 1: Speed comparison across different batch sizes
ax1 = axes[0]
x_pos = range(len(batch_sizes))

if gpu_times:
    # GPU available: show CPU vs GPU comparison
    width = 0.35
    bars1 = ax1.bar([p - width/2 for p in x_pos], cpu_times, width, label='CPU', color='#3498db', alpha=0.8)
    bars2 = ax1.bar([p + width/2 for p in x_pos], gpu_times, width, label='GPU', color='#e74c3c', alpha=0.8)
    ax1.set_title('Forward Propagation Time by Batch Size: CPU vs GPU', fontsize=12)

    # Annotate speedup on the bars
    for i, (cpu_t, gpu_t, sp) in enumerate(zip(cpu_times, gpu_times, speedups)):
        ax1.text(i, gpu_t * 0.5, f'{sp:.1f}x', ha='center', va='center',
                 fontsize=9, color='white')
else:
    # GPU not available: show only CPU data
    bars1 = ax1.bar(x_pos, cpu_times, color='#3498db', alpha=0.8)
    ax1.set_title('Forward Propagation Time by Batch Size: CPU (GPU not available)', fontsize=12)

    # Annotate time on the bars
    for i, cpu_t in enumerate(cpu_times):
        ax1.text(i, cpu_t * 0.5, f'{cpu_t:.1f}ms', ha='center', va='center',
                 fontsize=9, color='white')

ax1.set_xlabel('Batch Size', fontsize=11)
ax1.set_ylabel('Average Time (ms)', fontsize=11)
ax1.set_xticks(x_pos)
ax1.set_xticklabels(batch_sizes)
ax1.legend()
ax1.set_yscale('log')  # Use logarithmic scale
ax1.grid(True, alpha=0.3, axis='y')

# Figure 2: Speedup trend or CPU performance trend
ax2 = axes[1]

if speedups:
    # GPU available: show speedup trend
    ax2_twin = ax2.twinx()

    line1 = ax2.plot(batch_sizes, speedups, 'o-', color='#2ecc71', linewidth=2,
                     markersize=8, label='Batch Size vs Speedup')
    ax2.set_xlabel('Batch Size', fontsize=11)
    ax2.set_ylabel('Speedup (GPU/CPU)', fontsize=11, color='#2ecc71')
    ax2.tick_params(axis='y', labelcolor='#2ecc71')
    ax2.grid(True, alpha=0.3)

    if network_speedups:
        # Subplot 2b: Network scale vs speedup (using scatter points)
        x_pos_net = [i * max(batch_sizes) / 3 for i in range(len(network_speedups))]
        scatter = ax2_twin.scatter(x_pos_net, network_speedups, s=200, c='#f39c12',
                                   alpha=0.7, marker='s', label='Network Scale vs Speedup', zorder=5)
        ax2_twin.set_ylabel('Speedup (GPU/CPU)', fontsize=11, color='#f39c12')
        ax2_twin.tick_params(axis='y', labelcolor='#f39c12')

        # Add network scale labels
        for i, (x, y, label) in enumerate(zip(x_pos_net, network_speedups, network_labels)):
            ax2_twin.annotate(label, (x, y), xytext=(10, 10), textcoords='offset points',
                              fontsize=9, ha='left')

    ax2.set_title('GPU Speedup Trend Analysis', fontsize=12)

    # Add legend
    lines1, labels1 = ax2.get_legend_handles_labels()
    if network_speedups:
        lines2, labels2 = ax2_twin.get_legend_handles_labels()
        ax2.legend(lines1 + lines2, labels1 + labels2, loc='upper left')
    else:
        ax2.legend(loc='upper left')
else:
    # GPU not available: show CPU time vs batch size relationship
    ax2.plot(batch_sizes, cpu_times, 'o-', color='#3498db', linewidth=2,
             markersize=8, label='CPU Time')
    ax2.set_xlabel('Batch Size', fontsize=11)
    ax2.set_ylabel('Average Time (ms)', fontsize=11)
    ax2.set_title('CPU Forward Propagation Time Trend (GPU not available)', fontsize=12)
    ax2.grid(True, alpha=0.3)
    ax2.legend(loc='upper left')

plt.tight_layout()
plt.show()
plt.close()


# Output summary
print("\n" + "=" * 60)
print("Experiment Summary")
print("=" * 60)
print(f"1. Network configuration: {' -> '.join(map(str, layer_sizes))}")
print(f"2. Total parameters: {sum(p.numel() for p in model.parameters()):,}")
print(f"3. GPU available: {torch.cuda.is_available()}")
if torch.cuda.is_available():
    print(f"4. GPU device: {torch.cuda.get_device_name(0)}")
    print(f"5. Max speedup: {max(speedups):.1f}x (batch size {batch_sizes[speedups.index(max(speedups))]})")
    print(f"6. Average speedup: {sum(speedups)/len(speedups):.1f}x")
print("=" * 60)
Click Run to execute code. Click the code area to edit.

Summary

This chapter detailed the computational mechanism of forward propagation in neural networks, from the three-step process of a single neuron (input → weighted sum → activation transformation) to layer-by-layer transmission in multi-layer networks, from vector form to matrix form for batch processing, from mathematical formulas to the graphical representation of computational graphs. Forward propagation is the fundamental computational mechanism of neural networks, answering the question of how signals flow within the network — this is the inference process of neural networks. However, the problem of how network parameters (weights and biases) are determined remains unsolved, and this involves the training process of neural networks. The next chapter will introduce the backpropagation algorithm, revealing how neural networks learn to automatically adjust parameters, converting prediction errors into directions for parameter updates.

Exercises

  1. Given a three-layer neural network, input dimension n0=3n_0=3n0​=3, hidden layer neuron counts n1=5n_1=5n1​=5, n2=4n_2=4n2​=4, output dimension n3=2n_3=2n3​=2. Batch size B=10B=10B=10. Calculate the shapes of each layer's weight matrix and pre-activation matrix.

    Reference Answer

    Parameter shapes for each layer:

    • Layer 1 weight matrix W1\mathbf{W}^1W1: n1×n0=5×3n_1 \times n_0 = 5 \times 3n1​×n0​=5×3
    • Layer 1 bias vector b1\mathbf{b}^1b1: n1×1=5×1n_1 \times 1 = 5 \times 1n1​×1=5×1
    • Layer 2 weight matrix W2\mathbf{W}^2W2: n2×n1=4×5n_2 \times n_1 = 4 \times 5n2​×n1​=4×5
    • Layer 2 bias vector b2\mathbf{b}^2b2: n2×1=4×1n_2 \times 1 = 4 \times 1n2​×1=4×1
    • Layer 3 weight matrix W3\mathbf{W}^3W3: n3×n2=2×4n_3 \times n_2 = 2 \times 4n3​×n2​=2×4
    • Layer 3 bias vector b3\mathbf{b}^3b3: n3×1=2×1n_3 \times 1 = 2 \times 1n3​×1=2×1

    Pre-activation matrix shapes for each layer (batch size B=10B=10B=10):

    • Input matrix X\mathbf{X}X (i.e., A0\mathbf{A}^0A0): n0×B=3×10n_0 \times B = 3 \times 10n0​×B=3×10

    • Layer 1 pre-activation Z1=W1A0+b1\mathbf{Z}^1 = \mathbf{W}^1 \mathbf{A}^0 + \mathbf{b}^1Z1=W1A0+b1:

      • W1\mathbf{W}^1W1: 5×35 \times 35×3
      • A0\mathbf{A}^0A0: 3×103 \times 103×10
      • W1A0\mathbf{W}^1 \mathbf{A}^0W1A0: 5×105 \times 105×10
      • b1\mathbf{b}^1b1 (broadcast): 5×105 \times 105×10
      • Z1\mathbf{Z}^1Z1: 5×105 \times 105×10
    • Layer 1 activation A1\mathbf{A}^1A1: 5×105 \times 105×10

    • Layer 2 pre-activation Z2=W2A1+b2\mathbf{Z}^2 = \mathbf{W}^2 \mathbf{A}^1 + \mathbf{b}^2Z2=W2A1+b2:

      • W2\mathbf{W}^2W2: 4×54 \times 54×5
      • A1\mathbf{A}^1A1: 5×105 \times 105×10
      • W2A1\mathbf{W}^2 \mathbf{A}^1W2A1: 4×104 \times 104×10
      • Z2\mathbf{Z}^2Z2: 4×104 \times 104×10
    • Layer 2 activation A2\mathbf{A}^2A2: 4×104 \times 104×10

    • Layer 3 pre-activation Z3=W3A2+b3\mathbf{Z}^3 = \mathbf{W}^3 \mathbf{A}^2 + \mathbf{b}^3Z3=W3A2+b3:

      • W3\mathbf{W}^3W3: 2×42 \times 42×4
      • A2\mathbf{A}^2A2: 4×104 \times 104×10
      • W3A2\mathbf{W}^3 \mathbf{A}^2W3A2: 2×102 \times 102×10
      • Z3\mathbf{Z}^3Z3: 2×102 \times 102×10
    • Layer 3 activation A3\mathbf{A}^3A3 (output): 2×102 \times 102×10

Words: 4,579
Updated 2026-07-28
Last Updated:
Contributors: icyfenix, Claude
Prev
Multi-Layer Perceptron
Next
Backpropagation