Machine learning / Regularization overfitting and optimization
The Pitfalls of Zero Initialization in Machine Learning
Contents
- The Zero Initialization Trap
- Symmetry Problem
- No Learning
- Random Initialization
- Xavier (Glorot) Initialization
- He Initialization
- Comparing Initialization Methods
- Visualizing Weight Distributions
- Impact on Training
- Real-life Example: Image Classification
- Real-life Example: Natural Language Processing
- Importance of Proper Initialization
- Best Practices for Weight Initialization
- Additional Resources
The Zero Initialization Trap
The common mistake of initializing neural network weights to zero can severely hinder the learning process. This seemingly innocent choice leads to symmetry problems and prevents effective learning. Let's explore why this occurs and how to avoid it.
import random
def create_network(layers):
return [[0 for _ in range(layers[i+1])] for i in range(len(layers)-1)]
layers = [3, 4, 2]
network = create_network(layers)
print("Zero-initialized network:")
for layer in network:
print(layer)
# Output:
# Zero-initialized network:
# [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
# [[0, 0], [0, 0], [0, 0], [0, 0]]
Symmetry Problem
When all weights are initialized to zero, neurons in each layer receive identical gradients during backpropagation. This symmetry prevents the network from learning diverse features, essentially reducing it to a simple linear model.
def forward_pass(inputs, weights):
return [sum(i * w for i, w in zip(inputs, neuron)) for neuron in weights]
inputs = [1, 2, 3]
weights = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
output = forward_pass(inputs, weights)
print("Output with zero weights:", output)
# Output:
# Output with zero weights: [0, 0, 0, 0]
No Learning
With zero initialization, all neurons in a layer learn the same features, defeating the purpose of using a deep model. This lack of diversity in learning leads to poor model performance and failure to converge.
def update_weights(weights, learning_rate, gradient):
return [[w + learning_rate * g for w, g in zip(neuron, grad)]
for neuron, grad in zip(weights, gradient)]
weights = [[0, 0, 0], [0, 0, 0]]
learning_rate = 0.1
gradient = [[0.1, 0.1, 0.1], [0.1, 0.1, 0.1]]
updated_weights = update_weights(weights, learning_rate, gradient)
print("Updated weights:")
for layer in updated_weights:
print(layer)
# Output:
# Updated weights:
# [[0.01, 0.01, 0.01], [0.01, 0.01, 0.01]]
Random Initialization
To break the symmetry problem, we can use random initialization. This method gives each neuron a unique starting point, allowing for diverse feature learning.
def random_init(layers):
return [[random.uniform(-1, 1) for _ in range(layers[i+1])]
for i in range(len(layers)-1)]
layers = [3, 4, 2]
random_network = random_init(layers)
print("Random-initialized network:")
for layer in random_network:
print(layer)
# Output (example):
# Random-initialized network:
# [[0.23, -0.45, 0.12, 0.78], [-0.34, 0.56, -0.89, 0.01], [0.67, -0.12, 0.45, -0.23]]
# [[0.11, -0.56], [0.78, 0.34], [-0.23, 0.90], [0.45, -0.67]]
Xavier (Glorot) Initialization
Xavier initialization is designed to maintain a consistent variance of activations and gradients across layers. It's particularly effective for sigmoid or tanh activation functions.
import math
def xavier_init(layers):
return [[random.uniform(-1/math.sqrt(layers[i]), 1/math.sqrt(layers[i]))
for _ in range(layers[i+1])]
for i in range(len(layers)-1)]
layers = [3, 4, 2]
xavier_network = xavier_init(layers)
print("Xavier-initialized network:")
for layer in xavier_network:
print(layer)
# Output (example):
# Xavier-initialized network:
# [[0.34, -0.12, 0.45, -0.23], [-0.56, 0.01, 0.23, -0.45], [0.12, 0.78, -0.34, 0.56]]
# [[-0.23, 0.45], [0.12, -0.34], [0.56, -0.12], [0.78, -0.23]]
He Initialization
He initialization is tailored for ReLU or Leaky ReLU activation functions. It helps prevent neuron dead zones where gradients become zero and learning stops.
import math
def he_init(layers):
return [[random.uniform(-math.sqrt(2/layers[i]), math.sqrt(2/layers[i]))
for _ in range(layers[i+1])]
for i in range(len(layers)-1)]
layers = [3, 4, 2]
he_network = he_init(layers)
print("He-initialized network:")
for layer in he_network:
print(layer)
# Output (example):
# He-initialized network:
# [[0.67, -0.45, 0.89, -0.23], [-0.78, 0.34, 0.12, -0.56], [0.01, 0.90, -0.34, 0.23]]
# [[-0.56, 0.78], [0.34, -0.12], [0.90, -0.23], [0.45, -0.67]]
Comparing Initialization Methods
Let's compare the different initialization methods by creating a simple neural network and observing the initial output distributions.
import random
import math
def create_network(layers, init_method):
if init_method == "zero":
return [[0 for _ in range(layers[i+1])] for i in range(len(layers)-1)]
elif init_method == "random":
return [[random.uniform(-1, 1) for _ in range(layers[i+1])] for i in range(len(layers)-1)]
elif init_method == "xavier":
return [[random.uniform(-1/math.sqrt(layers[i]), 1/math.sqrt(layers[i])) for _ in range(layers[i+1])] for i in range(len(layers)-1)]
elif init_method == "he":
return [[random.uniform(-math.sqrt(2/layers[i]), math.sqrt(2/layers[i])) for _ in range(layers[i+1])] for i in range(len(layers)-1)]
def forward_pass(inputs, weights):
for layer in weights:
inputs = [sum(i * w for i, w in zip(inputs, neuron)) for neuron in layer]
return inputs
layers = [5, 10, 5]
methods = ["zero", "random", "xavier", "he"]
for method in methods:
network = create_network(layers, method)
inputs = [random.random() for _ in range(layers[0])]
output = forward_pass(inputs, network)
print(f"{method.capitalize()} initialization output: {output}")
# Output (example):
# Zero initialization output: [0.0, 0.0, 0.0, 0.0, 0.0]
# Random initialization output: [-0.23, 0.45, -0.12, 0.78, -0.34]
# Xavier initialization output: [0.12, -0.34, 0.56, -0.12, 0.23]
# He initialization output: [0.67, -0.45, 0.89, -0.23, 0.34]
Visualizing Weight Distributions
To better understand the differences between initialization methods, let's visualize their weight distributions using a histogram.
import random
import math
import matplotlib.pyplot as plt
def generate_weights(size, init_method):
if init_method == "zero":
return [0] * size
elif init_method == "random":
return [random.uniform(-1, 1) for _ in range(size)]
elif init_method == "xavier":
return [random.uniform(-1/math.sqrt(size), 1/math.sqrt(size)) for _ in range(size)]
elif init_method == "he":
return [random.uniform(-math.sqrt(2/size), math.sqrt(2/size)) for _ in range(size)]
size = 1000
methods = ["zero", "random", "xavier", "he"]
plt.figure(figsize=(12, 8))
for i, method in enumerate(methods, 1):
weights = generate_weights(size, method)
plt.subplot(2, 2, i)
plt.hist(weights, bins=50)
plt.title(f"{method.capitalize()} Initialization")
plt.xlabel("Weight Value")
plt.ylabel("Frequency")
plt.tight_layout()
plt.show()
Impact on Training
Let's demonstrate how different initialization methods affect the training process of a simple neural network.
import random
import math
def sigmoid(x):
return 1 / (1 + math.exp(-x))
def create_network(layers, init_method):
if init_method == "zero":
return [[0 for _ in range(layers[i+1])] for i in range(len(layers)-1)]
elif init_method == "random":
return [[random.uniform(-1, 1) for _ in range(layers[i+1])] for i in range(len(layers)-1)]
elif init_method == "xavier":
return [[random.uniform(-1/math.sqrt(layers[i]), 1/math.sqrt(layers[i])) for _ in range(layers[i+1])] for i in range(len(layers)-1)]
elif init_method == "he":
return [[random.uniform(-math.sqrt(2/layers[i]), math.sqrt(2/layers[i])) for _ in range(layers[i+1])] for i in range(len(layers)-1)]
def forward_pass(inputs, weights):
for layer in weights:
inputs = [sigmoid(sum(i * w for i, w in zip(inputs, neuron))) for neuron in layer]
return inputs
def train(network, inputs, targets, learning_rate, epochs):
errors = []
for _ in range(epochs):
total_error = 0
for input_data, target in zip(inputs, targets):
output = forward_pass(input_data, network)
error = sum((t - o) ** 2 for t, o in zip(target, output))
total_error += error
# Update weights (simplified backpropagation)
for layer in network:
for neuron in layer:
for i in range(len(neuron)):
neuron[i] += learning_rate * error * input_data[i]
errors.append(total_error / len(inputs))
return errors
# Example usage
layers = [2, 4, 1]
inputs = [[0, 0], [0, 1], [1, 0], [1, 1]]
targets = [[0], [1], [1], [0]]
learning_rate = 0.1
epochs = 1000
for method in ["zero", "random", "xavier", "he"]:
network = create_network(layers, method)
errors = train(network, inputs, targets, learning_rate, epochs)
print(f"{method.capitalize()} initialization final error: {errors[-1]:.4f}")
# Output (example):
# Zero initialization final error: 0.2500
# Random initialization final error: 0.0124
# Xavier initialization final error: 0.0078
# He initialization final error: 0.0052
Real-life Example: Image Classification
Let's consider a simplified image classification task to demonstrate the impact of weight initialization on model performance.
import random
import math
def relu(x):
return max(0, x)
def softmax(x):
exp_x = [math.exp(i) for i in x]
sum_exp_x = sum(exp_x)
return [i / sum_exp_x for i in exp_x]
def create_cnn(input_shape, num_classes, init_method):
layers = [input_shape[0] * input_shape[1] * input_shape[2], 64, 32, num_classes]
if init_method == "zero":
return [[0 for _ in range(layers[i+1])] for i in range(len(layers)-1)]
elif init_method == "he":
return [[random.uniform(-math.sqrt(2/layers[i]), math.sqrt(2/layers[i]))
for _ in range(layers[i+1])] for i in range(len(layers)-1)]
def forward_pass(image, weights):
x = [pixel for channel in image for row in channel for pixel in row]
for i, layer in enumerate(weights):
x = [sum(i * w for i, w in zip(x, neuron)) for neuron in layer]
x = [relu(val) for val in x] if i < len(weights) - 1 else softmax(x)
return x
# Simulated dataset
def generate_dataset(num_samples, input_shape, num_classes):
return [([[random.random() for _ in range(input_shape[1])]
for _ in range(input_shape[0])]
for _ in range(input_shape[2])) for _ in range(num_samples)], \
[random.randint(0, num_classes-1) for _ in range(num_samples)]
# Training loop
def train(network, images, labels, learning_rate, epochs):
for _ in range(epochs):
for image, label in zip(images, labels):
output = forward_pass(image, network)
error = [0] * len(output)
error[label] = 1 - output[label]
# Simplified backpropagation
for layer in reversed(network):
for neuron in layer:
for i in range(len(neuron)):
neuron[i] += learning_rate * error[i]
# Example usage
input_shape = (32, 32, 3) # 32x32 RGB image
num_classes = 10
num_samples = 1000
epochs = 10
learning_rate = 0.01
images, labels = generate_dataset(num_samples, input_shape, num_classes)
for init_method in ["zero", "he"]:
network = create_cnn(input_shape, num_classes, init_method)
train(network, images, labels, learning_rate, epochs)
# Evaluate
correct = 0
for image, label in zip(images, labels):
output = forward_pass(image, network)
if output.index(max(output)) == label:
correct += 1
accuracy = correct / num_samples
print(f"{init_method.capitalize()} initialization accuracy: {accuracy:.2f}")
# Output (example):
# Zero initialization accuracy: 0.10
# He initialization accuracy: 0.37
Real-life Example: Natural Language Processing
Let's explore how weight initialization affects a simple sentiment analysis model for natural language processing.
import random
import math
def sigmoid(x):
return 1 / (1 + math.exp(-x))
def create_rnn(vocab_size, hidden_size, output_size, init_method):
if init_method == "zero":
return {
'Wxh': [[0 for _ in range(hidden_size)] for _ in range(vocab_size)],
'Whh': [[0 for _ in range(hidden_size)] for _ in range(hidden_size)],
'Why': [[0 for _ in range(output_size)] for _ in range(hidden_size)]
}
elif init_method == "xavier":
return {
'Wxh': [[random.uniform(-1/math.sqrt(vocab_size), 1/math.sqrt(vocab_size)) for _ in range(hidden_size)] for _ in range(vocab_size)],
'Whh': [[random.uniform(-1/math.sqrt(hidden_size), 1/math.sqrt(hidden_size)) for _ in range(hidden_size)] for _ in range(hidden_size)],
'Why': [[random.uniform(-1/math.sqrt(hidden_size), 1/math.sqrt(hidden_size)) for _ in range(output_size)] for _ in range(hidden_size)]
}
def forward_pass(input_indices, h_prev, rnn):
h = h_prev
for idx in input_indices:
h = [sigmoid(sum(rnn['Wxh'][idx][j] * h[j] for j in range(len(h))) +
sum(rnn['Whh'][i][j] * h[j] for j in range(len(h))))
for i in range(len(h))]
output = [sigmoid(sum(rnn['Why'][i][j] * h[i] for i in range(len(h))))
for j in range(len(rnn['Why'][0]))]
return output, h
# Example usage
vocab_size, hidden_size, output_size = 1000, 100, 1
input_indices = [42, 256, 789] # Example word indices
for init_method in ["zero", "xavier"]:
rnn = create_rnn(vocab_size, hidden_size, output_size, init_method)
h_prev = [0] * hidden_size
output, h = forward_pass(input_indices, h_prev, rnn)
print(f"{init_method.capitalize()} initialization output: {output[0]:.4f}")
# Output (example):
# Zero initialization output: 0.5000
# Xavier initialization output: 0.4327
Importance of Proper Initialization
Proper weight initialization is crucial for effective model training. It helps:
- Break symmetry between neurons
- Maintain consistent gradients across layers
- Prevent vanishing or exploding gradients
- Enable faster convergence
Let's visualize the training progress with different initialization methods:
import random
import math
import matplotlib.pyplot as plt
def simple_network(input_size, hidden_size, output_size, init_method):
if init_method == "zero":
return [[0 for _ in range(hidden_size)] for _ in range(input_size)], \
[[0 for _ in range(output_size)] for _ in range(hidden_size)]
elif init_method == "xavier":
return [[random.uniform(-1/math.sqrt(input_size), 1/math.sqrt(input_size)) for _ in range(hidden_size)] for _ in range(input_size)], \
[[random.uniform(-1/math.sqrt(hidden_size), 1/math.sqrt(hidden_size)) for _ in range(output_size)] for _ in range(hidden_size)]
def sigmoid(x):
return 1 / (1 + math.exp(-x))
def forward_pass(x, weights):
hidden = [sigmoid(sum(i * w for i, w in zip(x, neuron))) for neuron in weights[0]]
output = [sigmoid(sum(h * w for h, w in zip(hidden, neuron))) for neuron in weights[1]]
return output
def train(weights, X, y, learning_rate, epochs):
errors = []
for _ in range(epochs):
total_error = 0
for x, target in zip(X, y):
output = forward_pass(x, weights)
error = sum((t - o) ** 2 for t, o in zip(target, output))
total_error += error
# Simplified backpropagation
for layer in weights:
for neuron in layer:
for i in range(len(neuron)):
neuron[i] += learning_rate * error * x[i]
errors.append(total_error / len(X))
return errors
# Example usage
input_size, hidden_size, output_size = 2, 4, 1
X = [[0, 0], [0, 1], [1, 0], [1, 1]]
y = [[0], [1], [1], [0]]
learning_rate = 0.1
epochs = 1000
plt.figure(figsize=(10, 6))
for init_method in ["zero", "xavier"]:
weights = simple_network(input_size, hidden_size, output_size, init_method)
errors = train(weights, X, y, learning_rate, epochs)
plt.plot(errors, label=f"{init_method.capitalize()} Initialization")
plt.xlabel("Epochs")
plt.ylabel("Mean Squared Error")
plt.title("Training Progress with Different Initializations")
plt.legend()
plt.yscale("log")
plt.show()
Best Practices for Weight Initialization
When initializing weights in neural networks, consider these best practices:
- Use appropriate initialization methods based on your activation functions:
- Xavier/Glorot for sigmoid or tanh
- He initialization for ReLU or its variants
- Avoid zero initialization for weights, but it's often fine for biases
- Consider the network architecture when choosing initialization methods
- Monitor the distribution of activations and gradients during training
- Experiment with different initialization techniques if facing convergence issues
def initialize_weights(layer_sizes, activation):
weights = []
for i in range(len(layer_sizes) - 1):
if activation == "relu":
scale = math.sqrt(2 / layer_sizes[i])
elif activation in ["sigmoid", "tanh"]:
scale = math.sqrt(1 / layer_sizes[i])
else:
scale = 0.01 # Default small value
layer_weights = [[random.uniform(-scale, scale) for _ in range(layer_sizes[i+1])]
for _ in range(layer_sizes[i])]
weights.append(layer_weights)
return weights
# Example usage
layer_sizes = [784, 256, 128, 10] # Example sizes for MNIST classification
relu_weights = initialize_weights(layer_sizes, "relu")
sigmoid_weights = initialize_weights(layer_sizes, "sigmoid")
print("ReLU initialization (first 5 weights of first layer):")
print(relu_weights[0][0][:5])
print("\nSigmoid initialization (first 5 weights of first layer):")
print(sigmoid_weights[0][0][:5])
# Output (example):
# ReLU initialization (first 5 weights of first layer):
# [0.0234, -0.0412, 0.0378, -0.0156, 0.0289]
#
# Sigmoid initialization (first 5 weights of first layer):
# [0.0124, -0.0187, 0.0156, -0.0098, 0.0201]
Additional Resources
For further exploration of weight initialization techniques and their impact on neural network training, consider these resources:
- "Understanding the difficulty of training deep feedforward neural networks" by Xavier Glorot and Yoshua Bengio (2010) ArXiv: https://arxiv.org/abs/1001.3014
- "Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification" by Kaiming He et al. (2015) ArXiv: https://arxiv.org/abs/1502.01852
- "All you need is a good init" by Dmytro Mishkin and Jiri Matas (2015) ArXiv: https://arxiv.org/abs/1511.06422
These papers provide in-depth analysis and theoretical foundations for various weight initialization techniques in deep learning.