Deep learning / Backpropagation and optimization
Chain Rule in Neural Network Backpropagation
Contents
- Introduction to the Chain Rule in Backpropagation
- The Chain Rule in Calculus
- Backpropagation Overview
- Forward Pass in Neural Networks
- Backward Pass and the Chain Rule
- Chain Rule in Multi-Layer Networks
- Gradient Accumulation
- Vanishing and Exploding Gradients
- Optimizing Backpropagation with Vectorization
- Real-Life Example: Image Classification
- Real-Life Example: Natural Language Processing
- Automatic Differentiation
- Challenges and Advanced Techniques
- Here's a simple example of gradient clipping:
- Conclusion
- Additional Resources
Introduction to the Chain Rule in Backpropagation
The chain rule is a fundamental concept in calculus that plays a crucial role in the backpropagation algorithm used to train neural networks. It allows us to compute the gradient of a composite function by breaking it down into simpler parts. In neural networks, this enables us to calculate how each parameter contributes to the overall error and update them accordingly.
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def sigmoid_derivative(x):
return sigmoid(x) * (1 - sigmoid(x))
# Example of a simple neural network forward pass
x = np.array([0.5, 0.3])
w1 = np.array([[0.1, 0.2], [0.3, 0.4]])
b1 = np.array([0.1, 0.1])
w2 = np.array([0.5, 0.6])
b2 = 0.1
z1 = np.dot(x, w1) + b1
a1 = sigmoid(z1)
z2 = np.dot(a1, w2) + b2
y_pred = sigmoid(z2)
print(f"Predicted output: {y_pred}")
The Chain Rule in Calculus
The chain rule states that for composite functions f(g(x)), the derivative is: (f ∘ g)'(x) = f'(g(x)) * g'(x)
This principle extends to multivariate functions and is the basis for backpropagation in neural networks. It allows us to compute gradients through multiple layers by decomposing complex derivatives into simpler ones.
def f(x):
return np.sin(x)
def g(x):
return x**2
def chain_rule_example(x):
# f(g(x)) = sin(x^2)
f_of_g = f(g(x))
# f'(g(x)) = cos(x^2)
f_prime_of_g = np.cos(g(x))
# g'(x) = 2x
g_prime = 2 * x
# Chain rule: (f ∘ g)'(x) = f'(g(x)) * g'(x)
chain_rule_result = f_prime_of_g * g_prime
return chain_rule_result
x = np.pi / 4
result = chain_rule_example(x)
print(f"Chain rule result at x = π/4: {result}")
Backpropagation Overview
Backpropagation is an algorithm that applies the chain rule to compute gradients in neural networks. It efficiently calculates the partial derivatives of the loss function with respect to each weight and bias in the network. This process occurs in two main steps: forward pass and backward pass.
class SimpleNeuralNetwork:
def __init__(self, input_size, hidden_size, output_size):
self.w1 = np.random.randn(input_size, hidden_size)
self.b1 = np.zeros((1, hidden_size))
self.w2 = np.random.randn(hidden_size, output_size)
self.b2 = np.zeros((1, output_size))
def forward(self, X):
self.z1 = np.dot(X, self.w1) + self.b1
self.a1 = sigmoid(self.z1)
self.z2 = np.dot(self.a1, self.w2) + self.b2
self.a2 = sigmoid(self.z2)
return self.a2
def backward(self, X, y, learning_rate):
m = X.shape[0]
# Backward pass
dz2 = self.a2 - y
dw2 = np.dot(self.a1.T, dz2) / m
db2 = np.sum(dz2, axis=0, keepdims=True) / m
dz1 = np.dot(dz2, self.w2.T) * sigmoid_derivative(self.z1)
dw1 = np.dot(X.T, dz1) / m
db1 = np.sum(dz1, axis=0, keepdims=True) / m
# Update weights and biases
self.w2 -= learning_rate * dw2
self.b2 -= learning_rate * db2
self.w1 -= learning_rate * dw1
self.b1 -= learning_rate * db1
# Usage example
nn = SimpleNeuralNetwork(2, 3, 1)
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y = np.array([[0], [1], [1], [0]])
for _ in range(10000):
y_pred = nn.forward(X)
nn.backward(X, y, learning_rate=0.1)
print("Final predictions:")
print(nn.forward(X))
Forward Pass in Neural Networks
The forward pass computes the output of the neural network given an input. It involves propagating the input through each layer, applying weights, biases, and activation functions. This step is crucial for both making predictions and setting up the backward pass.
import numpy as np
def relu(x):
return np.maximum(0, x)
class NeuralNetwork:
def __init__(self, layers):
self.layers = layers
self.weights = [np.random.randn(layers[i], layers[i+1]) for i in range(len(layers)-1)]
self.biases = [np.random.randn(1, layers[i+1]) for i in range(len(layers)-1)]
def forward(self, X):
self.activations = [X]
self.z_values = []
for i in range(len(self.weights)):
z = np.dot(self.activations[-1], self.weights[i]) + self.biases[i]
self.z_values.append(z)
a = relu(z) if i < len(self.weights) - 1 else sigmoid(z)
self.activations.append(a)
return self.activations[-1]
# Example usage
nn = NeuralNetwork([2, 3, 2, 1])
X = np.array([[0.5, 0.3]])
output = nn.forward(X)
print(f"Network output: {output}")
Backward Pass and the Chain Rule
The backward pass applies the chain rule to compute gradients of the loss function with respect to each parameter. It starts from the output layer and moves backwards, accumulating gradients layer by layer. This process allows us to update weights and biases to minimize the loss.
def mse_loss(y_true, y_pred):
return np.mean((y_true - y_pred)**2)
def mse_loss_derivative(y_true, y_pred):
return 2 * (y_pred - y_true) / y_true.size
class NeuralNetwork:
# ... (previous methods remain the same)
def backward(self, X, y, learning_rate):
m = X.shape[0]
# Compute the gradient of the loss with respect to the output
dL_da = mse_loss_derivative(y, self.activations[-1])
for i in reversed(range(len(self.weights))):
if i == len(self.weights) - 1:
dz = dL_da * sigmoid_derivative(self.z_values[i])
else:
dz = dL_da * (self.z_values[i] > 0) # ReLU derivative
dw = np.dot(self.activations[i].T, dz) / m
db = np.sum(dz, axis=0, keepdims=True) / m
# Update weights and biases
self.weights[i] -= learning_rate * dw
self.biases[i] -= learning_rate * db
# Prepare dL_da for the next layer
dL_da = np.dot(dz, self.weights[i].T)
# Example usage
nn = NeuralNetwork([2, 3, 2, 1])
X = np.array([[0.5, 0.3], [0.1, 0.8]])
y = np.array([[1], [0]])
for _ in range(1000):
y_pred = nn.forward(X)
nn.backward(X, y, learning_rate=0.1)
print(f"Final predictions: {nn.forward(X)}")
Chain Rule in Multi-Layer Networks
In deep neural networks with multiple layers, the chain rule becomes particularly important. It allows us to propagate gradients through many layers, accounting for the effect of each parameter on the final output. This slide demonstrates how gradients are computed across multiple layers.
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def sigmoid_derivative(x):
return sigmoid(x) * (1 - sigmoid(x))
# Define a 3-layer neural network
x = np.array([0.5, 0.3])
w1 = np.array([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]])
b1 = np.array([0.1, 0.1, 0.1])
w2 = np.array([[0.7, 0.8], [0.9, 1.0], [1.1, 1.2]])
b2 = np.array([0.2, 0.2])
w3 = np.array([[1.3], [1.4]])
b3 = np.array([0.3])
# Forward pass
z1 = np.dot(x, w1) + b1
a1 = sigmoid(z1)
z2 = np.dot(a1, w2) + b2
a2 = sigmoid(z2)
z3 = np.dot(a2, w3) + b3
y_pred = sigmoid(z3)
# Backward pass (assuming binary cross-entropy loss)
y_true = np.array([1])
dL_dy = -(y_true / y_pred - (1 - y_true) / (1 - y_pred))
dy_dz3 = sigmoid_derivative(z3)
dz3_dw3 = a2
dz3_db3 = 1
dz3_da2 = w3.T
# Chain rule for w3
dL_dw3 = dL_dy * dy_dz3 * dz3_dw3
# Chain rule for b3
dL_db3 = dL_dy * dy_dz3 * dz3_db3
# Chain rule for a2
dL_da2 = dL_dy * dy_dz3 * dz3_da2
# Continue backpropagation to earlier layers...
print(f"Gradient of loss with respect to w3: {dL_dw3}")
print(f"Gradient of loss with respect to b3: {dL_db3}")
Gradient Accumulation
In complex networks, gradients from multiple paths can contribute to a single parameter. The chain rule allows us to accumulate these gradients correctly. This slide demonstrates how gradients are accumulated for shared parameters in a simple network with skip connections.
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def sigmoid_derivative(x):
return sigmoid(x) * (1 - sigmoid(x))
# Network with skip connection
x = np.array([0.5, 0.3])
w1 = np.array([[0.1, 0.2], [0.3, 0.4]])
b1 = np.array([0.1, 0.1])
w2 = np.array([[0.5, 0.6], [0.7, 0.8]])
b2 = np.array([0.2, 0.2])
w_skip = np.array([[0.9, 1.0], [1.1, 1.2]])
# Forward pass
z1 = np.dot(x, w1) + b1
a1 = sigmoid(z1)
z2 = np.dot(a1, w2) + b2 + np.dot(x, w_skip)
y_pred = sigmoid(z2)
# Backward pass
y_true = np.array([1, 0])
dL_dy = y_pred - y_true
dy_dz2 = sigmoid_derivative(z2)
dz2_dw2 = a1
dz2_db2 = 1
dz2_da1 = w2.T
dz2_dw_skip = x
# Gradients for w2 and b2
dL_dw2 = np.outer(dz2_dw2, dL_dy * dy_dz2)
dL_db2 = dL_dy * dy_dz2
# Gradient for w_skip
dL_dw_skip = np.outer(dz2_dw_skip, dL_dy * dy_dz2)
# Gradient for a1 (accumulating from both paths)
dL_da1 = np.dot(dL_dy * dy_dz2, dz2_da1)
# Continue backpropagation to w1 and b1...
print(f"Gradient of loss with respect to w2: {dL_dw2}")
print(f"Gradient of loss with respect to w_skip: {dL_dw_skip}")
Vanishing and Exploding Gradients
The chain rule in deep networks can lead to vanishing or exploding gradients. This occurs when many small (< 1) or large (> 1) values are multiplied during backpropagation. Let's demonstrate how gradient values can change dramatically across layers and how techniques like gradient clipping can help.
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def sigmoid_derivative(x):
return sigmoid(x) * (1 - sigmoid(x))
# Deep network example
np.random.seed(42)
layers = [10] * 20 # 20 layers with 10 neurons each
weights = [np.random.randn(layers[i], layers[i+1]) / np.sqrt(layers[i]) for i in range(len(layers)-1)]
biases = [np.zeros(layers[i+1]) for i in range(len(layers)-1)]
# Forward pass
x = np.random.randn(layers[0])
activations = [x]
for w, b in zip(weights, biases):
z = np.dot(activations[-1], w) + b
a = sigmoid(z)
activations.append(a)
# Backward pass
gradients = [np.random.randn(layers[-1])]
for i in reversed(range(len(weights))):
grad = gradients[-1]
z = np.dot(activations[i], weights[i]) + biases[i]
dz = grad * sigmoid_derivative(z)
dw = np.outer(activations[i], dz)
db = dz
da = np.dot(dz, weights[i].T)
gradients.append(da)
# Print gradient norms
for i, grad in enumerate(reversed(gradients)):
print(f"Layer {i} gradient norm: {np.linalg.norm(grad)}")
# Gradient clipping
clip_value = 1.0
clipped_gradients = [np.clip(grad, -clip_value, clip_value) for grad in gradients]
print("\nAfter clipping:")
for i, grad in enumerate(reversed(clipped_gradients)):
print(f"Layer {i} clipped gradient norm: {np.linalg.norm(grad)}")
Optimizing Backpropagation with Vectorization
Vectorization is crucial for efficient implementation of the chain rule in neural networks. It allows for parallel computation of gradients across multiple training examples. This slide demonstrates how to vectorize the forward and backward passes for a batch of inputs.
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def sigmoid_derivative(x):
return sigmoid(x) * (1 - sigmoid(x))
class VectorizedNeuralNetwork:
def __init__(self, layer_sizes):
self.weights = [np.random.randn(i, j) / np.sqrt(i) for i, j in zip(layer_sizes[:-1], layer_sizes[1:])]
self.biases = [np.zeros((1, j)) for j in layer_sizes[1:]]
def forward(self, X):
self.activations = [X]
self.z_values = []
for w, b in zip(self.weights, self.biases):
z = np.dot(self.activations[-1], w) + b
self.z_values.append(z)
a = sigmoid(z)
self.activations.append(a)
return self.activations[-1]
def backward(self, X, y, learning_rate):
m = X.shape[0]
delta = self.activations[-1] - y
for i in reversed(range(len(self.weights))):
dw = np.dot(self.activations[i].T, delta) / m
db = np.sum(delta, axis=0, keepdims=True) / m
self.weights[i] -= learning_rate * dw
self.biases[i] -= learning_rate * db
if i > 0:
delta = np.dot(delta, self.weights[i].T) * sigmoid_derivative(self.z_values[i-1])
# Example usage
nn = VectorizedNeuralNetwork([2, 3, 1])
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y = np.array([[0], [1], [1], [0]])
for _ in range(10000):
y_pred = nn.forward(X)
nn.backward(X, y, learning_rate=0.1)
print("Final predictions:")
print(nn.forward(X))
Real-Life Example: Image Classification
Let's apply the chain rule and backpropagation to a simple image classification task. We'll use a small convolutional neural network to classify handwritten digits from the MNIST dataset.
import numpy as np
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
# Load and preprocess data
digits = load_digits()
X = digits.images.reshape((-1, 8, 8, 1)) / 16.0
y = np.eye(10)[digits.target]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
def conv2d(X, W, b, stride=1):
h, w = X.shape[1] - W.shape[0] + 1, X.shape[2] - W.shape[1] + 1
Y = np.zeros((X.shape[0], h, w, W.shape[3]))
for i in range(0, h, stride):
for j in range(0, w, stride):
Y[:, i, j, :] = np.sum(X[:, i:i+W.shape[0], j:j+W.shape[1], :, np.newaxis] * W, axis=(1, 2, 3)) + b
return Y
def softmax(x):
exp_x = np.exp(x - np.max(x, axis=1, keepdims=True))
return exp_x / np.sum(exp_x, axis=1, keepdims=True)
class SimpleCNN:
def __init__(self):
self.W1 = np.random.randn(3, 3, 1, 16) * 0.01
self.b1 = np.zeros((1, 1, 1, 16))
self.W2 = np.random.randn(6 * 6 * 16, 10) * 0.01
self.b2 = np.zeros((1, 10))
def forward(self, X):
self.Z1 = conv2d(X, self.W1, self.b1)
self.A1 = np.maximum(0, self.Z1) # ReLU activation
self.Z2 = np.dot(self.A1.reshape(X.shape[0], -1), self.W2) + self.b2
self.A2 = softmax(self.Z2)
return self.A2
def backward(self, X, y, learning_rate):
m = X.shape[0]
dZ2 = self.A2 - y
dW2 = np.dot(self.A1.reshape(m, -1).T, dZ2) / m
db2 = np.sum(dZ2, axis=0, keepdims=True) / m
dA1 = np.dot(dZ2, self.W2.T).reshape(self.A1.shape)
dZ1 = dA1 * (self.Z1 > 0) # ReLU derivative
dW1 = np.zeros_like(self.W1)
for i in range(self.W1.shape[0]):
for j in range(self.W1.shape[1]):
dW1[i, j, :, :] = np.sum(X[:, i:i+6, j:j+6, :, np.newaxis] * dZ1[:, :, :, np.newaxis, :], axis=(0, 1, 2)) / m
db1 = np.sum(dZ1, axis=(0, 1, 2), keepdims=True) / m
self.W2 -= learning_rate * dW2
self.b2 -= learning_rate * db2
self.W1 -= learning_rate * dW1
self.b1 -= learning_rate * db1
# Training
cnn = SimpleCNN()
for epoch in range(100):
y_pred = cnn.forward(X_train)
cnn.backward(X_train, y_train, learning_rate=0.01)
if epoch % 10 == 0:
loss = -np.sum(y_train * np.log(y_pred + 1e-8)) / X_train.shape[0]
print(f"Epoch {epoch}, Loss: {loss:.4f}")
# Evaluation
y_pred = cnn.forward(X_test)
accuracy = np.mean(np.argmax(y_pred, axis=1) == np.argmax(y_test, axis=1))
print(f"Test Accuracy: {accuracy:.4f}")
Real-Life Example: Natural Language Processing
In this example, we'll apply the chain rule and backpropagation to a simple recurrent neural network (RNN) for sentiment analysis. We'll use a small dataset of movie reviews to demonstrate how backpropagation through time (BPTT) works in sequence models.
import numpy as np
# Sample data (simplified for demonstration)
vocab = {'good': 0, 'bad': 1, 'movie': 2, 'the': 3, 'is': 4}
X = [[0, 2, 4, 0], [1, 2, 4, 1]] # "good movie is good", "bad movie is bad"
y = [1, 0] # Positive, Negative
class SimpleRNN:
def __init__(self, input_size, hidden_size, output_size):
self.Wxh = np.random.randn(hidden_size, input_size) * 0.01
self.Whh = np.random.randn(hidden_size, hidden_size) * 0.01
self.Why = np.random.randn(output_size, hidden_size) * 0.01
self.bh = np.zeros((hidden_size, 1))
self.by = np.zeros((output_size, 1))
def forward(self, inputs):
h = np.zeros((self.Whh.shape[0], 1))
self.hs = []
for x in inputs:
h = np.tanh(np.dot(self.Wxh, x) + np.dot(self.Whh, h) + self.bh)
self.hs.append(h)
y = np.dot(self.Why, h) + self.by
p = 1 / (1 + np.exp(-y)) # Sigmoid activation
return p, h
def backward(self, inputs, target, learning_rate):
dWxh, dWhh, dWhy = np.zeros_like(self.Wxh), np.zeros_like(self.Whh), np.zeros_like(self.Why)
dbh, dby = np.zeros_like(self.bh), np.zeros_like(self.by)
dhnext = np.zeros_like(self.hs[0])
p, _ = self.forward(inputs)
dy = p - target
dWhy += np.dot(dy, self.hs[-1].T)
dby += dy
for t in reversed(range(len(inputs))):
dh = np.dot(self.Why.T, dy) + dhnext
dhraw = (1 - self.hs[t] ** 2) * dh
dbh += dhraw
dWxh += np.dot(dhraw, inputs[t].T)
dWhh += np.dot(dhraw, self.hs[t-1].T) if t > 0 else np.dot(dhraw, np.zeros_like(self.hs[0]).T)
dhnext = np.dot(self.Whh.T, dhraw)
for dparam in [dWxh, dWhh, dWhy, dbh, dby]:
np.clip(dparam, -5, 5, out=dparam) # Clip gradients
self.Wxh -= learning_rate * dWxh
self.Whh -= learning_rate * dWhh
self.Why -= learning_rate * dWhy
self.bh -= learning_rate * dbh
self.by -= learning_rate * dby
# Training
rnn = SimpleRNN(input_size=len(vocab), hidden_size=10, output_size=1)
for epoch in range(1000):
total_loss = 0
for inputs, target in zip(X, y):
x_one_hot = np.zeros((len(inputs), len(vocab)))
for t, word_idx in enumerate(inputs):
x_one_hot[t, word_idx] = 1
p, _ = rnn.forward(x_one_hot)
loss = -np.log(p[0] if target == 1 else 1 - p[0])
total_loss += loss
rnn.backward(x_one_hot, target, learning_rate=0.1)
if epoch % 100 == 0:
print(f"Epoch {epoch}, Loss: {total_loss:.4f}")
# Test
test_sentence = [0, 2, 4, 0] # "good movie is good"
x_test = np.zeros((len(test_sentence), len(vocab)))
for t, word_idx in enumerate(test_sentence):
x_test[t, word_idx] = 1
p, _ = rnn.forward(x_test)
print(f"Sentiment prediction for 'good movie is good': {p[0]}")
Automatic Differentiation
While we've implemented backpropagation manually, modern deep learning frameworks use automatic differentiation to compute gradients. This technique applies the chain rule automatically, making it easier to implement and experiment with complex models. Here's a simple example using PyTorch.
import torch
import torch.nn as nn
import torch.optim as optim
class SimpleNet(nn.Module):
def __init__(self):
super(SimpleNet, self).__init__()
self.fc1 = nn.Linear(2, 3)
self.fc2 = nn.Linear(3, 1)
def forward(self, x):
x = torch.relu(self.fc1(x))
x = torch.sigmoid(self.fc2(x))
return x
# Create model, loss function, and optimizer
model = SimpleNet()
criterion = nn.BCELoss()
optimizer = optim.SGD(model.parameters(), lr=0.1)
# Sample data
X = torch.tensor([[0., 0.], [0., 1.], [1., 0.], [1., 1.]])
y = torch.tensor([[0.], [1.], [1.], [0.]])
# Training loop
for epoch in range(1000):
# Forward pass
y_pred = model(X)
loss = criterion(y_pred, y)
# Backward pass and optimization
optimizer.zero_grad()
loss.backward()
optimizer.step()
if epoch % 100 == 0:
print(f"Epoch {epoch}, Loss: {loss.item():.4f}")
# Test the model
with torch.no_grad():
test_input = torch.tensor([[0., 0.], [1., 1.]])
predictions = model(test_input)
print("Predictions:")
print(predictions)
Challenges and Advanced Techniques
While the chain rule and backpropagation are powerful tools for training neural networks, they can face challenges in very deep or complex architectures. Here are some advanced techniques that address these issues:
- Gradient Clipping: Prevents exploding gradients by limiting their magnitude.
- Batch Normalization: Normalizes layer inputs, helping with internal covariate shift.
- Residual Connections: Allow gradients to flow more easily through deep networks.
- LSTM and GRU: Address vanishing gradients in recurrent neural networks.
Here's a simple example of gradient clipping:
import torch
import torch.nn as nn
import torch.optim as optim
class DeepNet(nn.Module):
def __init__(self):
super(DeepNet, self).__init__()
self.layers = nn.Sequential(
*[nn.Linear(10, 10) for _ in range(20)],
nn.Linear(10, 1)
)
def forward(self, x):
return self.layers(x)
model = DeepNet()
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)
# Sample data
X = torch.randn(100, 10)
y = torch.randn(100, 1)
# Training loop with gradient clipping
for epoch in range(100):
optimizer.zero_grad()
output = model(X)
loss = criterion(output, y)
loss.backward()
# Gradient clipping
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
if epoch % 10 == 0:
print(f"Epoch {epoch}, Loss: {loss.item():.4f}")
Conclusion
The chain rule is a fundamental concept in calculus that enables the training of neural networks through backpropagation. By breaking down complex derivatives into simpler parts, we can efficiently compute gradients and update model parameters. This process allows neural networks to learn from data and make accurate predictions.
Key takeaways:
- The chain rule allows us to compute gradients through multiple layers.
- Backpropagation applies the chain rule to efficiently calculate gradients.
- Vectorization improves the efficiency of gradient computations.
- Automatic differentiation in modern frameworks simplifies implementation.
- Advanced techniques address challenges in training deep networks.
As neural networks continue to evolve, the principles of the chain rule and backpropagation remain crucial for understanding and improving deep learning models.
Additional Resources
For those interested in diving deeper into the chain rule, backpropagation, and neural networks, here are some valuable resources:
- "Deep Learning" by Ian Goodfellow, Yoshua Bengio, and Aaron Courville ArXiv: https://arxiv.org/abs/1206.5533
- "Efficient BackProp" by Yann LeCun, Leon Bottou, Genevieve B. Orr, and Klaus-Robert Müller ArXiv: https://arxiv.org/abs/1206.5533v2
- "Random Search for Hyper-Parameter Optimization" by James Bergstra and Yoshua Bengio ArXiv: https://arxiv.org/abs/1412.6980
- "Adam: A Method for Stochastic Optimization" by Diederik P. Kingma and Jimmy Ba ArXiv: https://arxiv.org/abs/1412.6980v8
- "Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift" by Sergey Ioffe and Christian Szegedy ArXiv: https://arxiv.org/abs/1502.03167
These resources provide in-depth explanations and advanced techniques related to the topics covered in this presentation.