xbe.at

Deep learning / Neural network fundamentals

My Notes About Neural Network Design in a Comprehensive Way

12 min read, 14 sections

Contents
  1. Title Slide
  2. Maximum Likelihood and Cross-Entropy Loss
  3. Python Code - Cross-Entropy Loss Implementation
  4. Activation Functions - ReLU and Variants
  5. Python Code - ReLU and Variants Implementation
  6. Maxout Units
  7. Python Code - Maxout Unit Implementation
  8. Architecture Design - Universal Approximation Theorem
  9. PyTorch vs. TensorFlow - Computational Graphs
  10. Python Code - PyTorch Dynamic Graph Example
  11. Backpropagation Overview
  12. Deep Learning Renaissance (2005)
  13. Best Practices in Neural Network Design
  14. Conclusion and Future Directions

Title Slide

Neural Network Training: Loss Functions, Activation Functions, and Architecture Design

Maximum Likelihood and Cross-Entropy Loss

Neural networks are typically trained according to Maximum Likelihood Distribution due to the loss function we use, known as cross-entropy loss.

The Maximum Likelihood Distribution is given by:

J(θ) = -E[log p_model(y|x)]

Where:

  • J(θ) is the cost function
  • E denotes the expected value
  • p_model(y|x) is the probability that the model predicts y given x

Cross-entropy loss is defined as:

L = -Σ y_i * log(p_i)

Where:

  • L is the loss
  • y_i is the true label
  • p_i is the predicted probability

Advantages of cross-entropy loss:

  1. It doesn't saturate (become flat)
  2. No model in practice gives p = 0 or p = 1, so the minimum is never reached
  3. It's well-suited for classification tasks
  4. It works well with softmax activation in the output layer

Alternative loss functions like mean absolute error or mean squared error can lead to poor results as some outputs saturate and slow down training.

Python Code - Cross-Entropy Loss Implementation

import torch
import torch.nn as nn

def custom_cross_entropy_loss(predictions, targets):
    epsilon = 1e-10
    predictions = torch.clamp(predictions, min=epsilon, max=1-epsilon)
    N = predictions.shape[0]
    ce_loss = -torch.sum(targets * torch.log(predictions)) / N
    return ce_loss

# Example usage
predictions = torch.tensor([[0.2, 0.7, 0.1], [0.9, 0.05, 0.05]])
targets = torch.tensor([[0, 1, 0], [1, 0, 0]])

custom_loss = custom_cross_entropy_loss(predictions, targets)
print(f"Custom Cross-Entropy Loss: {custom_loss.item()}")

# Compare with PyTorch's built-in CrossEntropyLoss
ce_loss = nn.CrossEntropyLoss()
torch_loss = ce_loss(predictions, torch.argmax(targets, dim=1))
print(f"PyTorch Cross-Entropy Loss: {torch_loss.item()}")

This code demonstrates a custom implementation of cross-entropy loss and compares it with PyTorch's built-in CrossEntropyLoss. The custom implementation adds a small epsilon value to prevent log(0) errors and uses torch.clamp to ensure numerical stability.

Activation Functions - ReLU and Variants

Rectified Linear Unit (ReLU): ReLU(x) = max(0, x)

Properties and benefits of ReLU:

  1. Simple computation: f(x) = x if x > 0, else 0
  2. Non-linear activation that allows for complex function approximation
  3. Sparsity: Negative inputs are mapped to zero
  4. Reduces vanishing gradient problem common in sigmoid/tanh
  5. Computationally efficient: only comparison, addition and multiplication

ReLU variants:

  1. Leaky ReLU: f(x) = αx for x < 0, x for x ≥ 0 Where α is a small constant, typically 0.01
  2. Parametric ReLU (PReLU): f(x) = αx for x < 0, x for x ≥ 0 Where α is a learnable parameter

Benefits of ReLU variants:

  • Address the "dying ReLU" problem where neurons can get stuck in a state where they never activate
  • Allow for small negative values, potentially preserving important features

The choice of activation function is often based on experimentation, with ReLU being a common default choice for hidden layers in many architectures.

Python Code - ReLU and Variants Implementation

import torch
import torch.nn as nn
import torch.nn.functional as F

class CustomReLU(nn.Module):
    def forward(self, x):
        return torch.max(torch.tensor(0.0), x)

class CustomLeakyReLU(nn.Module):
    def __init__(self, negative_slope=0.01):
        super().__init__()
        self.negative_slope = negative_slope

    def forward(self, x):
        return torch.where(x > 0, x, self.negative_slope * x)

class CustomPReLU(nn.Module):
    def __init__(self, num_parameters=1, init=0.25):
        super().__init__()
        self.weight = nn.Parameter(torch.full((num_parameters,), init))

    def forward(self, x):
        return F.prelu(x, self.weight)

# Example usage
x = torch.randn(5)
print("Input:", x)

relu = CustomReLU()
leaky_relu = CustomLeakyReLU(0.1)
prelu = CustomPReLU()

print("ReLU:", relu(x))
print("Leaky ReLU:", leaky_relu(x))
print("PReLU:", prelu(x))

This code provides custom implementations of ReLU, Leaky ReLU, and PReLU activation functions. The CustomReLU class uses torch.max to implement the standard ReLU function. CustomLeakyReLU uses torch.where for conditional evaluation, allowing a small slope for negative inputs. CustomPReLU uses nn.Parameter to create a learnable parameter for the negative slope, which can be optimized during training.

Maxout Units

Maxout Units are a type of activation function introduced by Goodfellow et al. in 2013. They are defined as:

h_i(x) = max_{j ∈ [1,k]} z_{ij}

Where:

  • h_i(x) is the output of the i-th maxout unit
  • z_{ij} = x^T W_{...ij} + b_{ij}
  • W is a weight matrix and b is a bias vector
  • k is the number of pieces to max over

Properties of Maxout Units:

  1. They can approximate any convex function
  2. They introduce more complexity than ReLU
  3. They are particularly effective when used with dropout regularization
  4. They don't have a fixed activation shape, allowing for more flexible function approximation

Comparison with ReLU:

  • Maxout units can learn the activation function, while ReLU has a fixed form
  • Maxout units can represent linear functions perfectly, which ReLU cannot
  • Maxout units are more computationally expensive than ReLU
  • Maxout units may be better at avoiding catastrophic forgetting in some scenarios

The principle behind Maxout and ReLU variants is that units which are closer to linearity often perform better in practice.

Python Code - Maxout Unit Implementation

import torch
import torch.nn as nn

class MaxoutUnit(nn.Module):
    def __init__(self, in_features, out_features, num_pieces):
        super().__init__()
        self.in_features = in_features
        self.out_features = out_features
        self.num_pieces = num_pieces
        self.linear = nn.Linear(in_features, out_features * num_pieces)

    def forward(self, x):
        shape = list(x.size())
        shape[-1] = self.out_features
        shape.append(self.num_pieces)
        x = self.linear(x)
        x = x.view(*shape)
        x, _ = torch.max(x, -1)
        return x

# Example usage
maxout = MaxoutUnit(10, 5, 3)
input_tensor = torch.randn(32, 10)  # Batch size of 32, input size of 10
output = maxout(input_tensor)
print("Input shape:", input_tensor.shape)
print("Maxout output shape:", output.shape)

# Visualize the effect of Maxout on random data
import matplotlib.pyplot as plt

x = torch.linspace(-5, 5, 1000).unsqueeze(1)
maxout_1d = MaxoutUnit(1, 1, 3)
y = maxout_1d(x).detach().numpy()

plt.figure(figsize=(10, 5))
plt.plot(x.numpy(), y)
plt.title("Maxout Unit Output")
plt.xlabel("Input")
plt.ylabel("Output")
plt.grid(True)
plt.show()

This code implements a Maxout Unit as a PyTorch module. The forward method reshapes the output of a linear layer and applies the max operation along the last dimension. The example usage demonstrates how to use the Maxout Unit in a network and visualizes its output for 1D input data.

Architecture Design - Universal Approximation Theorem

The Universal Approximation Theorem states that a feedforward network with a single hidden layer containing a finite number of neurons can approximate any continuous function on compact subsets of R^n, under mild assumptions on the activation function.

Formal statement: Let φ(x) be a nonconstant, bounded, and continuous activation function. Let I_m denote the m-dimensional unit hypercube [0,1]^m. The space of continuous functions on I_m is denoted by C(I_m). Then, given any ε > 0 and any function f ∈ C(I_m), there exist an integer N, real constants v_i, b_i ∈ R and real vectors w_i ∈ R^m, where i = 1, ..., N such that we may define:

F(x) = Σ_{i=1}^N v_i φ(w_i^T x + b_i)

as an approximate realization of the function f; that is,

|F(x) - f(x)| < ε

for all x ∈ I_m.

Implications:

  1. Neural networks have the potential to approximate any continuous function
  2. The theorem guarantees the existence of a network but doesn't provide a method to find it
  3. In practice, deeper networks often perform better than wide, shallow networks

Depth vs. Width:

  • Deeper networks can represent certain functions more efficiently than wider networks
  • Deep networks can learn hierarchical features
  • The number of parameters grows linearly with depth but quadratically with width
  • Very deep networks may face optimization challenges (vanishing/exploding gradients)

Rule of thumb: Deeper networks tend to generalize better than wider networks with the same number of parameters.

PyTorch vs. TensorFlow - Computational Graphs

PyTorch (Dynamic Graphs):

  • Builds a dynamic computation graph on-the-fly during the forward pass
  • Graph is reconstructed from scratch for every forward pass
  • Allows for more flexible and intuitive debugging
  • Well-suited for variable-length inputs and dynamic network architectures

TensorFlow (Static Graphs):

  • Builds a static computation graph before the model runs
  • Graph is compiled once and then reused for all subsequent runs
  • Can be more efficient for repeated computations on fixed-size inputs
  • Easier to optimize and deploy in production environments

Comparison:

  1. Flexibility:
    • PyTorch: More flexible, easier to modify graphs
    • TensorFlow: Less flexible, but potentially more optimized
  2. Debugging:
    • PyTorch: Easier to debug with standard Python tools
    • TensorFlow: Debugging can be more challenging due to the separate graph-building phase
  3. Performance:
    • PyTorch: Can be faster for dynamic architectures
    • TensorFlow: Often faster for static, fixed-size computations
  4. Deployment:
    • PyTorch: Improving with tools like TorchScript
    • TensorFlow: Traditionally stronger, with tools like TensorFlow Serving
  5. Community and Ecosystem:
    • Both have large, active communities
    • TensorFlow has a slight edge in production deployment tools
    • PyTorch is often preferred in research settings

Choice between PyTorch and TensorFlow often depends on the specific requirements of the project and personal preference.

Python Code - PyTorch Dynamic Graph Example

import torch
import torch.nn as nn

class DynamicNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.hidden = nn.Linear(10, 20)
        self.output = nn.Linear(20, 1)

    def forward(self, x):
        h = torch.relu(self.hidden(x))
        y = self.output(h)
        if torch.sum(y) > 0:
            y = y * 2
        return y

# Example usage
model = DynamicNet()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

for epoch in range(5):
    x = torch.randn(32, 10)
    y_pred = model(x)
    loss = torch.sum(y_pred)
    print(f"Epoch {epoch}, Loss: {loss.item()}")

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

# Demonstrate dynamic computation
x1 = torch.randn(32, 10)
y1 = model(x1)
print(f"Output sum for x1: {torch.sum(y1).item()}")

x2 = torch.randn(32, 10)
y2 = model(x2)
print(f"Output sum for x2: {torch.sum(y2).item()}")

This example demonstrates PyTorch's dynamic computation graph:

  1. The DynamicNet class defines a simple network with a conditional operation in the forward pass.
  2. The model's behavior changes based on the output sum, showcasing dynamic computation.
  3. We can easily modify the network structure or add conditional operations without rebuilding the entire graph.
  4. The backward pass automatically handles the dynamic structure, computing gradients correctly.

Backpropagation Overview

Backpropagation is a key algorithm for training neural networks, efficiently computing gradients of the loss function with respect to the network parameters.

Key Concepts:

  1. Forward Pass: Compute the output and loss
  2. Backward Pass: Compute gradients using the chain rule
  3. Parameter Update: Adjust weights and biases based on gradients

Mathematically, for a loss function L and parameters θ: ∂L/∂θ = ∂L/∂y * ∂y/∂θ

Where:

  • ∂L/∂y is the gradient of the loss with respect to the output
  • ∂y/∂θ is the gradient of the output with respect to the parameters

Backpropagation Algorithm:

  1. Initialize network parameters randomly
  2. For each training example: a. Perform forward pass to compute activations b. Compute the loss c. Perform backward pass to compute gradients d. Update parameters: θ = θ - η * ∂L/∂θ (η is the learning rate)
  3. Repeat until convergence or for a fixed number of epochs

Advantages:

  • Efficient computation of gradients
  • Allows for training of deep neural networks
  • Scales well with network size and dataset size

Challenges:

  • Vanishing/exploding gradients in very deep networks
  • Local minima and saddle points in the loss landscape
  • Requires careful initialization and hyperparameter tuning

Backpropagation is often used in conjunction with optimization algorithms like Stochastic Gradient Descent (SGD) or its variants (Adam, RMSprop, etc.) to train neural networks effectively.

Deep Learning Renaissance (2005)

The Deep Learning Renaissance, beginning around 2005, marked a resurgence of interest and breakthroughs in neural network research. Key factors contributing to this renaissance include:

  1. Big Datasets:
    • Availability of large-scale, labeled datasets (e.g., ImageNet)
    • Enabled training of more complex models with better generalization
  2. Increased Computational Power:
    • GPU acceleration for neural network training
    • Distributed computing and cloud infrastructure
  3. Algorithmic Improvements:
    • Better initialization techniques (e.g., Xavier/Glorot initialization)
    • Improved activation functions (ReLU and its variants)
    • Advanced optimization algorithms (Adam, RMSprop)
  4. Architectural Innovations:
    • Deep Belief Networks and unsupervised pre-training
    • Convolutional Neural Networks (CNNs) for computer vision
    • Recurrent Neural Networks (RNNs) and LSTM for sequential data
  5. Transfer Learning and Pre-training:
    • Ability to leverage knowledge from one task to another
    • Self-supervised learning techniques
  6. Industry and Academic Collaboration:
    • Increased investment from tech companies
    • Open-source frameworks and tools (TensorFlow, PyTorch)
  7. Breakthroughs in Application Areas:
    • Computer Vision (ImageNet competition)
    • Natural Language Processing (word embeddings, transformers)
    • Reinforcement Learning (DeepMind's AlphaGo)

The Deep Learning Renaissance led to significant improvements in various fields, including computer vision, natural language processing, speech recognition, and robotics, paving the way for the current era of AI-driven technologies.

Best Practices in Neural Network Design

  1. Use of Piece-wise Linear Hidden Units:
    • Prefer ReLU and its variants over sigmoid/tanh for hidden layers
    • Benefits: Faster training, reduced vanishing gradient problem
    • Exception: Use tanh/sigmoid for specific architectures (e.g., LSTMs)
  2. Loss Function Selection:
    • Prefer cross-entropy loss over Mean Squared Error (MSE) for classification
    • Use appropriate loss functions for regression (MSE, MAE) or generative models (KL-divergence)
  3. Network Architecture:
    • Start with established architectures for your domain (e.g., ResNet for vision)
    • Balance depth and width based on your problem complexity and data size
    • Consider skip connections for very deep networks
  4. Initialization:
    • Use Xavier/Glorot initialization for tanh activations
    • Use He initialization for ReLU activations
  5. Regularization:
    • Apply dropout to prevent overfitting
    • Use weight decay (L2 regularization) to constrain model complexity
    • Consider batch normalization to stabilize training
  6. Optimization:
    • Start with adaptive optimizers like Adam for faster convergence
    • Fine-tune with SGD with momentum for potentially better generalization
  7. Learning Rate Schedule:
    • Use learning rate decay or cyclic learning rates
    • Consider warmup periods for very deep networks
  8. Data Augmentation:
    • Apply domain-specific augmentations to increase effective dataset size
    • Helps improve model generalization and robustness
  9. Ensemble Methods:
    • Combine multiple models for improved performance and robustness
  10. Monitoring and Visualization:
  • Use tools like TensorBoard to track training progress
  • Visualize activations and gradients to diagnose issues
  1. Hyperparameter Tuning:
  • Use systematic approaches like grid search, random search, or Bayesian optimization
  • Consider the trade-off between model complexity and dataset size

These best practices should be adapted based on specific problem requirements and empirical results. Continuous experimentation and staying updated with the latest research are crucial for effective neural network design.

Conclusion and Future Directions

Conclusion:

  • Neural networks have become a powerful tool for solving complex problems in various domains
  • Understanding loss functions, activation functions, and architecture design is crucial for effective implementation
  • Best practices in neural network design continue to evolve with ongoing research and practical applications

Future Directions:

  1. Efficient Architectures:
    • Neural Architecture Search (NAS) for automated design
    • Compact models for edge devices and mobile applications
  2. Interpretability and Explainability:
    • Developing techniques to understand and visualize network decisions
    • Addressing the "black box" nature of deep learning models
  3. Robustness and Adversarial Defense:
    • Creating models resilient to adversarial attacks
    • Improving generalization to out-of-distribution data
  4. Few-shot and Zero-shot Learning:
    • Enabling models to learn from limited examples
    • Transferring knowledge across domains more effectively
  5. Unsupervised and Self-supervised Learning:
    • Leveraging unlabeled data for improved representations
    • Reducing dependence on large labeled datasets
  6. Ethical AI and Fairness:
    • Addressing biases in training data and model outputs
    • Developing frameworks for responsible AI development
  7. Energy-efficient Deep Learning:
    • Reducing the carbon footprint of training large models
    • Optimizing inference for reduced energy consumption
  8. Neuroscience-inspired Architectures:
    • Incorporating insights from brain research into neural network design
    • Developing more biologically plausible learning algorithms
  9. Quantum Machine Learning:
    • Exploring the intersection of quantum computing and neural networks
    • Developing quantum-inspired classical algorithms
  10. Continual Learning:
  • Creating models that can learn continuously without forgetting
  • Addressing the stability-plasticity dilemma in neural networks

More in Neural network fundamentals

  1. Neural Networks and Deep Learning Concepts8 min

    Neural networks are the backbone of deep learning, inspired by the human brain's structure. They consist of interconnected nodes (neurons) organized in...

  2. Mathematical Theory of Deep Learning17 min

    Deep learning has revolutionized machine learning, achieving remarkable success in various domains. This mathematical theory, developed by Philipp Petersen...

  3. Neural Networks as Universal Function Approximators8 min

    Neural networks fundamentally operate as universal function approximators, capable of learning complex mappings between inputs and outputs through...

  4. Mastering Multi-Layer Perceptron (MLP) with Python14 min

    A Multi-Layer Perceptron is a feedforward artificial neural network that consists of multiple layers of nodes, including an input layer, one or more hidden...

  5. Neural Networks as Universal Function Approximators in Python10 min

    Neural networks are powerful models capable of approximating any continuous function to arbitrary precision. This property, known as universal approximation...

  6. Machine Learning vs Neural Networks8 min

    Machine Learning (ML) and Neural Networks (NN) are both subfields of Artificial Intelligence, but they differ in their approach and capabilities. This...