Deep learning / Neural network fundamentals
My Notes About Neural Network Design in a Comprehensive Way
Contents
- Title Slide
- Maximum Likelihood and Cross-Entropy Loss
- Python Code - Cross-Entropy Loss Implementation
- Activation Functions - ReLU and Variants
- Python Code - ReLU and Variants Implementation
- Maxout Units
- Python Code - Maxout Unit Implementation
- Architecture Design - Universal Approximation Theorem
- PyTorch vs. TensorFlow - Computational Graphs
- Python Code - PyTorch Dynamic Graph Example
- Backpropagation Overview
- Deep Learning Renaissance (2005)
- Best Practices in Neural Network Design
- 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:
- It doesn't saturate (become flat)
- No model in practice gives p = 0 or p = 1, so the minimum is never reached
- It's well-suited for classification tasks
- 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:
- Simple computation: f(x) = x if x > 0, else 0
- Non-linear activation that allows for complex function approximation
- Sparsity: Negative inputs are mapped to zero
- Reduces vanishing gradient problem common in sigmoid/tanh
- Computationally efficient: only comparison, addition and multiplication
ReLU variants:
- Leaky ReLU: f(x) = αx for x < 0, x for x ≥ 0 Where α is a small constant, typically 0.01
- 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:
- They can approximate any convex function
- They introduce more complexity than ReLU
- They are particularly effective when used with dropout regularization
- 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:
- Neural networks have the potential to approximate any continuous function
- The theorem guarantees the existence of a network but doesn't provide a method to find it
- 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:
- Flexibility:
- PyTorch: More flexible, easier to modify graphs
- TensorFlow: Less flexible, but potentially more optimized
- Debugging:
- PyTorch: Easier to debug with standard Python tools
- TensorFlow: Debugging can be more challenging due to the separate graph-building phase
- Performance:
- PyTorch: Can be faster for dynamic architectures
- TensorFlow: Often faster for static, fixed-size computations
- Deployment:
- PyTorch: Improving with tools like TorchScript
- TensorFlow: Traditionally stronger, with tools like TensorFlow Serving
- 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:
- The DynamicNet class defines a simple network with a conditional operation in the forward pass.
- The model's behavior changes based on the output sum, showcasing dynamic computation.
- We can easily modify the network structure or add conditional operations without rebuilding the entire graph.
- 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:
- Forward Pass: Compute the output and loss
- Backward Pass: Compute gradients using the chain rule
- 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:
- Initialize network parameters randomly
- 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)
- 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:
- Big Datasets:
- Availability of large-scale, labeled datasets (e.g., ImageNet)
- Enabled training of more complex models with better generalization
- Increased Computational Power:
- GPU acceleration for neural network training
- Distributed computing and cloud infrastructure
- Algorithmic Improvements:
- Better initialization techniques (e.g., Xavier/Glorot initialization)
- Improved activation functions (ReLU and its variants)
- Advanced optimization algorithms (Adam, RMSprop)
- 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
- Transfer Learning and Pre-training:
- Ability to leverage knowledge from one task to another
- Self-supervised learning techniques
- Industry and Academic Collaboration:
- Increased investment from tech companies
- Open-source frameworks and tools (TensorFlow, PyTorch)
- 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
- 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)
- 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)
- 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
- Initialization:
- Use Xavier/Glorot initialization for tanh activations
- Use He initialization for ReLU activations
- Regularization:
- Apply dropout to prevent overfitting
- Use weight decay (L2 regularization) to constrain model complexity
- Consider batch normalization to stabilize training
- Optimization:
- Start with adaptive optimizers like Adam for faster convergence
- Fine-tune with SGD with momentum for potentially better generalization
- Learning Rate Schedule:
- Use learning rate decay or cyclic learning rates
- Consider warmup periods for very deep networks
- Data Augmentation:
- Apply domain-specific augmentations to increase effective dataset size
- Helps improve model generalization and robustness
- Ensemble Methods:
- Combine multiple models for improved performance and robustness
- Monitoring and Visualization:
- Use tools like TensorBoard to track training progress
- Visualize activations and gradients to diagnose issues
- 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:
- Efficient Architectures:
- Neural Architecture Search (NAS) for automated design
- Compact models for edge devices and mobile applications
- Interpretability and Explainability:
- Developing techniques to understand and visualize network decisions
- Addressing the "black box" nature of deep learning models
- Robustness and Adversarial Defense:
- Creating models resilient to adversarial attacks
- Improving generalization to out-of-distribution data
- Few-shot and Zero-shot Learning:
- Enabling models to learn from limited examples
- Transferring knowledge across domains more effectively
- Unsupervised and Self-supervised Learning:
- Leveraging unlabeled data for improved representations
- Reducing dependence on large labeled datasets
- Ethical AI and Fairness:
- Addressing biases in training data and model outputs
- Developing frameworks for responsible AI development
- Energy-efficient Deep Learning:
- Reducing the carbon footprint of training large models
- Optimizing inference for reduced energy consumption
- Neuroscience-inspired Architectures:
- Incorporating insights from brain research into neural network design
- Developing more biologically plausible learning algorithms
- Quantum Machine Learning:
- Exploring the intersection of quantum computing and neural networks
- Developing quantum-inspired classical algorithms
- Continual Learning:
- Creating models that can learn continuously without forgetting
- Addressing the stability-plasticity dilemma in neural networks