Deep learning / Neural network fundamentals
Importance of Proper Neural Network Initialization
Contents
- Understanding Neural Network Weight Initialization
- Xavier/Glorot Initialization
- He Initialization Implementation
- Implementing a Neural Network with Different Initializations
- Source Code for Neural Network Implementation
- Real-World Example - MNIST Digit Classification
- Source Code for MNIST Training Loop
- Comparative Analysis of Initialization Methods
- Advanced Weight Initialization Techniques
- Gradient Flow Analysis
- Implementing Batch Normalization with Initialization
- Real-World Example - Transfer Learning Initialization
- Additional Resources
Understanding Neural Network Weight Initialization
Neural network initialization is crucial for model convergence and training stability. Zero initialization creates symmetry problems where neurons receive identical gradients, leading to ineffective learning. Proper initialization breaks this symmetry and enables distinct feature learning across neurons.
import numpy as np
class LayerInitializer:
def zero_init(input_dim, output_dim):
"""Demonstrates problematic zero initialization"""
return np.zeros((input_dim, output_dim))
def uniform_init(input_dim, output_dim, scale=0.01):
"""Random uniform initialization"""
return np.random.uniform(
-scale, scale,
size=(input_dim, output_dim)
)
# Example usage
input_dim, output_dim = 784, 256
zero_weights = LayerInitializer.zero_init(input_dim, output_dim)
random_weights = LayerInitializer.uniform_init(input_dim, output_dim)
print("Zero init variance:", np.var(zero_weights))
print("Random init variance:", np.var(random_weights))
Xavier/Glorot Initialization
Xavier initialization maintains consistent variance of activations and gradients throughout the network, particularly effective for sigmoid and tanh activation functions. It scales weights based on the number of input and output connections to prevent vanishing/exploding gradients.
def xavier_init(input_dim, output_dim):
"""Xavier/Glorot initialization for sigmoid/tanh"""
limit = np.sqrt(6 / (input_dim + output_dim))
return np.random.uniform(
-limit, limit,
size=(input_dim, output_dim)
)
# Demonstrate Xavier initialization properties
input_dims = [100, 1000, 10000]
output_dim = 500
for dim in input_dims:
weights = xavier_init(dim, output_dim)
print(f"Input dim {dim}:")
print(f"Weight variance: {np.var(weights):.6f}")
print(f"Weight mean: {np.mean(weights):.6f}\n")
He Initialization Implementation
He initialization, designed specifically for ReLU activation functions, scales weights to maintain stable gradients by accounting for the ReLU's positive-only output. This method helps achieve faster convergence in deep networks with ReLU activations.
def he_init(input_dim, output_dim):
"""He initialization for ReLU networks"""
std = np.sqrt(2.0 / input_dim)
return np.random.normal(
0, std,
size=(input_dim, output_dim)
)
# Compare He vs Xavier for different layer sizes
layer_sizes = [(784, 256), (256, 128), (128, 10)]
for idx, (in_dim, out_dim) in enumerate(layer_sizes):
he_weights = he_init(in_dim, out_dim)
xavier_weights = xavier_init(in_dim, out_dim)
print(f"Layer {idx + 1} ({in_dim} -> {out_dim}):")
print(f"He variance: {np.var(he_weights):.6f}")
print(f"Xavier variance: {np.var(xavier_weights):.6f}\n")
Implementing a Neural Network with Different Initializations
A complete implementation of a simple neural network that demonstrates the impact of different initialization methods. The network architecture includes multiple layers with configurable activation functions and initialization strategies.
class NeuralNetwork:
def __init__(self, layer_dims, init_method='he'):
self.weights = []
self.biases = []
for i in range(len(layer_dims) - 1):
if init_method == 'he':
W = he_init(layer_dims[i], layer_dims[i+1])
elif init_method == 'xavier':
W = xavier_init(layer_dims[i], layer_dims[i+1])
else:
W = np.random.randn(layer_dims[i], layer_dims[i+1]) * 0.01
b = np.zeros((1, layer_dims[i+1]))
self.weights.append(W)
self.biases.append(b)
Source Code for Neural Network Implementation
def relu(self, Z):
return np.maximum(0, Z)
def relu_derivative(self, Z):
return np.where(Z > 0, 1, 0)
def forward_propagation(self, X):
self.activations = [X]
A = X
for i in range(len(self.weights)):
Z = np.dot(A, self.weights[i]) + self.biases[i]
A = self.relu(Z)
self.activations.append(A)
return A
def compute_loss(self, y_pred, y_true):
return np.mean(np.square(y_pred - y_true))
# Example usage
nn = NeuralNetwork([784, 256, 128, 10], init_method='he')
X = np.random.randn(100, 784) # Example input
output = nn.forward_propagation(X)
print(f"Output shape: {output.shape}")
Real-World Example - MNIST Digit Classification
Implementation of a complete neural network for MNIST digit classification, demonstrating the impact of different initialization techniques on model convergence and accuracy. This example showcases practical application of initialization methods.
import numpy as np
from sklearn.datasets import fetch_openml
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
class MNISTClassifier:
def __init__(self, init_method='he'):
self.init_method = init_method
self.layers = [784, 128, 64, 10]
self.network = NeuralNetwork(self.layers, init_method)
def preprocess_data(self):
# Load MNIST data
X, y = fetch_openml('mnist_784', version=1, return_X_y=True, as_frame=False)
X = X.astype('float32') / 255.0
# Split and scale data
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
return X_train, X_test, y_train, y_test
Source Code for MNIST Training Loop
def train(self, X_train, y_train, epochs=10, batch_size=32):
n_samples = X_train.shape[0]
history = {'loss': [], 'accuracy': []}
for epoch in range(epochs):
# Shuffle training data
indices = np.random.permutation(n_samples)
X_shuffled = X_train[indices]
y_shuffled = y_train[indices]
# Mini-batch training
for i in range(0, n_samples, batch_size):
X_batch = X_shuffled[i:i + batch_size]
y_batch = y_shuffled[i:i + batch_size]
# Forward pass
output = self.network.forward_propagation(X_batch)
loss = self.network.compute_loss(output, y_batch)
# Backward pass and update
self.network.backward_propagation(y_batch)
self.network.update_parameters(learning_rate=0.01)
# Record metrics
train_loss = self.evaluate(X_train, y_train)
history['loss'].append(train_loss)
return history
Comparative Analysis of Initialization Methods
We implement a systematic comparison of different initialization techniques on the MNIST dataset, measuring convergence speed, final accuracy, and gradient flow characteristics across network layers during training.
def compare_initializations(X_train, y_train, X_test, y_test):
init_methods = ['he', 'xavier', 'random']
results = {}
for method in init_methods:
print(f"\nTraining with {method} initialization...")
classifier = MNISTClassifier(init_method=method)
# Time training and collect metrics
start_time = time.time()
history = classifier.train(X_train, y_train)
train_time = time.time() - start_time
# Evaluate
test_accuracy = classifier.evaluate(X_test, y_test)
results[method] = {
'training_time': train_time,
'test_accuracy': test_accuracy,
'loss_history': history['loss']
}
return results
Advanced Weight Initialization Techniques
Modern neural networks often require specialized initialization strategies beyond basic methods. Here we implement orthogonal initialization and scaled initialization with noise adaptation for deep architectures.
def orthogonal_init(shape):
"""
Orthogonal initialization for weight matrices
Particularly useful for RNNs and very deep networks
"""
rand_matrix = np.random.randn(*shape)
# Perform QR factorization
q, r = np.linalg.qr(rand_matrix)
# Ensure deterministic behavior
d = np.diag(r)
ph = np.sign(d)
q *= ph
if shape[0] < shape[1]:
return q.T
return q
def scaled_init_with_noise(shape, scale=1.0, noise_std=0.01):
"""
Scaled initialization with controlled noise injection
"""
fan_in, fan_out = shape
limit = np.sqrt(6 / ((fan_in + fan_out) * scale))
weights = np.random.uniform(-limit, limit, shape)
# Add controlled noise
noise = np.random.normal(0, noise_std, shape)
return weights + noise
Gradient Flow Analysis
Implementation of tools to analyze gradient flow through networks with different initialization methods. This helps understand how different initialization strategies affect gradient propagation during training.
class GradientFlowAnalyzer:
def __init__(self, model):
self.model = model
self.gradient_stats = []
def analyze_gradients(self, layer_outputs):
"""Analyze gradient magnitudes across layers"""
gradients = []
for idx, layer in enumerate(self.model.weights):
grad_norm = np.linalg.norm(layer)
variance = np.var(layer)
gradients.append({
'layer': idx,
'norm': grad_norm,
'variance': variance
})
return gradients
def track_training_dynamics(self, X_batch):
"""Track activation patterns during forward pass"""
activations = self.model.forward_propagation(X_batch)
activation_stats = []
for idx, activation in enumerate(activations):
stats = {
'mean': np.mean(activation),
'std': np.std(activation),
'dead_neurons': np.mean(activation == 0)
}
activation_stats.append(stats)
return activation_stats
Implementing Batch Normalization with Initialization
Understanding how batch normalization interacts with weight initialization and implementing a robust initialization strategy that accounts for normalization layers.
class BatchNormLayer:
def __init__(self, input_dim, epsilon=1e-8):
self.gamma = np.ones(input_dim)
self.beta = np.zeros(input_dim)
self.epsilon = epsilon
# Running statistics
self.running_mean = np.zeros(input_dim)
self.running_var = np.ones(input_dim)
def normalize(self, X, training=True):
if training:
batch_mean = np.mean(X, axis=0)
batch_var = np.var(X, axis=0)
# Update running statistics
self.running_mean = 0.9 * self.running_mean + 0.1 * batch_mean
self.running_var = 0.9 * self.running_var + 0.1 * batch_var
else:
batch_mean = self.running_mean
batch_var = self.running_var
X_norm = (X - batch_mean) / np.sqrt(batch_var + self.epsilon)
return self.gamma * X_norm + self.beta
# Example usage with different initializations
input_dim = 256
batch_norm = BatchNormLayer(input_dim)
X = np.random.randn(32, input_dim)
normalized_output = batch_norm.normalize(X)
Real-World Example - Transfer Learning Initialization
Implementation of transfer learning initialization strategies, demonstrating how to properly initialize new layers while maintaining pretrained weights in a neural network.
class TransferLearningInitializer:
def __init__(self, pretrained_weights, new_layer_dims):
self.pretrained_weights = pretrained_weights
self.new_layer_dims = new_layer_dims
def initialize_new_layers(self):
"""Initialize new layers while preserving pretrained weights"""
new_layers = []
for idx, (in_dim, out_dim) in enumerate(self.new_layer_dims):
# Scale initialization based on pretrained statistics
if idx == 0:
# Connect to pretrained layers
std = np.std(self.pretrained_weights[-1])
weights = np.random.normal(0, std, (in_dim, out_dim))
else:
# He initialization for intermediate layers
weights = he_init(in_dim, out_dim)
new_layers.append(weights)
return new_layers
# Example usage
pretrained = [np.random.randn(784, 512), np.random.randn(512, 256)]
new_layers = [(256, 128), (128, 10)]
transfer_init = TransferLearningInitializer(pretrained, new_layers)
new_weights = transfer_init.initialize_new_layers()
Additional Resources
- Deep Learning Weight Initialization: A Mathematical Analysis
- https://arxiv.org/abs/1906.04721
- Understanding the Difficulty of Training Deep Feedforward Neural Networks
- https://arxiv.org/abs/1502.01852
- Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification
- https://arxiv.org/abs/1502.01852
- Weight Initialization in Neural Networks: A Journey From the Basics to Kaiming
- https://arxiv.org/abs/1412.6558
- On Weight Initialization in Deep Neural Networks
- https://arxiv.org/abs/1704.08863