Welcome back! Over the past few weeks, we've built a strong foundation in machine learning. We started with linear regression, where we learned to predict continuous values by finding the best-fitting line through our data. Then we moved to logistic regression, where we tackled binary classification problems by using the sigmoid function to map our predictions to probabilities between 0 and 1.
Let's quickly recall the structure of logistic regression:
where is the sigmoid function.
Notice the elegant pattern here: we take a linear combination of inputs (the weighted sum), then apply a non-linear activation function (sigmoid). This simple formula has solved many real-world problems: spam detection, disease diagnosis, customer churn prediction, and more.
But here's the thing: logistic regression draws a single decision boundary through your data. Sometimes that's perfect. Other times... not so much.
Imagine you're trying to classify data where the patterns are more complex. What if the relationship between features and outcomes isn't captured by a single straight line or plane? What if you need multiple decision boundaries working together?
This is where we hit a fundamental limitation of logistic regression. No matter how we adjust the weights, we're always drawing just one line (or hyperplane in higher dimensions) to separate our classes.
Today, we're going to discover something powerful: what if we stack multiple logistic regression units together? What if we let one set of units learn useful intermediate representations, and then let another unit make the final decision based on those representations?
This idea—composition of simple operations—is at the heart of neural networks.
In this lecture, we'll take a natural step forward from what we already know:
For MPS311 students: By the end, you'll be able to build and train neural networks using Keras, understand when to use them, and interpret their behavior.
For MPS439 students: You'll additionally understand the backpropagation algorithm that makes training efficient, and you'll learn advanced techniques like regularization and dropout.
Let's begin with a classic challenge that will illuminate everything.
Let me introduce you to a deceptively simple problem that revolutionized our understanding of neural computation. It's called the XOR problem (exclusive OR).
XOR is a logical operation that outputs true (1) when inputs differ, and false (0) when they're the same. Here's the truth table:
| XOR Output | ||
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
Think of a practical example: An alarm system that triggers if either a door is open or a window is open, but not both (because if both are open, it might be you entering through the door while airing out the room).
Now here's the puzzle: can we use logistic regression to learn this XOR function? Let's visualize the problem:

Look at those four points. The blue circles (class 0) are at opposite corners, and the red crosses (class 1) are at the other opposite corners. Try to imagine drawing a single straight line that separates blue from red. Go ahead, try it.
You can't! Any line you draw will misclassify at least one point.
Let's think about this mathematically. For logistic regression to work, we need to find weights , , and bias such that:
From the second and third constraints, we need to be large (positive) and to be large (positive). But then would also be large and positive, contradicting the fourth constraint!
This problem is not linearly separable. No single line can solve it.
Here's an intuitive question: what if we could use two lines instead of one? What if we had one line that captures " is on but is off" and another line that captures " is on but is off", and then we combined these insights?
If we could do that, we could solve XOR! But how do we make logistic regression use multiple decision boundaries?
The answer: we don't use one logistic regression unit—we use multiple units arranged in layers. Let some units learn intermediate patterns, and then let another unit combine those patterns.
This is our entry point into neural networks.
Let's be crystal clear about our building block. A single logistic regression unit computes:
Geometrically, this creates one decision boundary—a hyperplane in the input space. Points on one side get classified as one class, points on the other side as the other class.
Now here's the beautiful idea: instead of going directly from inputs to output, what if we introduce an intermediate layer of processing?

Look at the difference:
The intermediate layer is called a hidden layer because it's not directly visible in the input or output—it's hidden inside the network, learning useful representations.
Let's make this concrete. Suppose we have two hidden units. Each one is just a logistic regression unit:
Now, instead of predicting from and directly, we predict from and :
Here's the magic: might learn to detect " AND NOT " (fires when and ). Meanwhile, might learn to detect " AND NOT " (fires when and ). Then the output unit simply says: "Output 1 if either or is active."

This diagram shows the complete architecture for solving XOR. The two hidden units learn complementary patterns, and the output unit combines them. This is a neural network!
This example illustrates a profound principle: by stacking layers of simple operations, we can approximate arbitrarily complex functions.
Each layer transforms the data into a new representation. The first hidden layer might learn simple patterns (edges, basic combinations). Deeper layers could learn more abstract concepts (shapes, complex relationships). The final layer makes the decision based on these learned representations.
And here's what's remarkable: each individual unit is just doing logistic regression! We haven't invented anything fundamentally new at the unit level. The power comes from composition—putting simple pieces together in the right way.
Let's formalize what we've discovered. A neural network consists of:
Input Layer: This isn't really a "layer" in terms of computation—it's just our features . If you have 10 features, you have 10 input nodes.
Hidden Layer(s): One or more layers of computational units. Each unit in a hidden layer:
Output Layer: The final layer that produces predictions. For binary classification, this is typically one unit with sigmoid activation.

This diagram shows a network with 3 inputs, one hidden layer with 4 units, and 1 output. Notice:
Let's establish our vocabulary:
Neuron/Unit: A single computational unit. It receives inputs, computes a weighted sum, applies an activation function, and produces an output. Think of it as one logistic regression unit.
Weights (): The parameters that connect layers. denotes the weights connecting layer to layer . These are what the network learns during training.
Biases (): The offset parameters for each unit. denotes the biases for layer .
Activation: The output of a neuron after applying the activation function. We often denote the activation of layer as .
Let's write this precisely. For a network with one hidden layer:
Hidden layer computation:
where:
Output layer computation:
where:
General form for any layer :
This compact notation describes the entire computation! Each layer takes the previous layer's activation, applies a linear transformation (via and ), and applies an activation function.
Understanding matrix shapes: If layer has units and layer has units, then has shape . This ensures the matrix multiplication works out correctly.
You've probably heard the term "deep learning." What does "deep" mean?
The term "deep" simply refers to having multiple layers stacked on top of each other. Deep networks can learn more complex, hierarchical representations. However, they're also harder to train and require more data.
For this lecture, we'll focus on shallow networks (1 hidden layer). But the principles we learn extend directly to deeper architectures!
Here's a critical question: why do we need activation functions at all? Why not just compute ?
Let's see what happens without activation functions. Expanding the expression:
Notice that is just another matrix (call it ), and is just another vector (call it ). So we get:
This is just linear regression! No matter how many layers we stack, without non-linear activation functions, we'd just be doing a fancy version of linear regression. All those layers would collapse into a single linear transformation.
The non-linearity is what gives neural networks their power. The activation function introduces the non-linearity that allows networks to learn complex patterns.
Let's look at the most common activation functions you'll encounter:

1. Sigmoid:
2. ReLU (Rectified Linear Unit):
3. Tanh (Hyperbolic Tangent):
Here's a simple decision guide:
For hidden layers:
For output layer:
Rule of thumb: When in doubt, use ReLU for all hidden layers. Adjust only if you have specific reasons.
Forward propagation is the process of computing the network's output from its input. It's called "forward" because we move forward through the network: input → hidden layer(s) → output.
Think of it like an assembly line:
Each layer takes what the previous layer produced, transforms it, and passes it forward.
Let's work through a complete example with actual numbers. This will make everything concrete.
Network architecture:
Given weights and biases (normally these are learned, but we'll specify them):
Input:
Step 1: Compute hidden layer pre-activation
Step 2: Apply ReLU activation
(Both values are positive, so ReLU doesn't change them)
Step 3: Compute output layer pre-activation
Step 4: Apply sigmoid activation
Final prediction:
For binary classification, we'd typically threshold at 0.5, so this would predict class 1.
In practice, we rarely process one example at a time. We process batches of examples simultaneously. This is much more efficient, especially on GPUs.
Instead of being a single column vector, becomes a matrix where each row is one example:
where is the batch size.
The forward propagation equations remain almost identical:
Now each matrix operation processes all examples at once! This is why GPUs (which excel at matrix operations) make neural networks practical.
Another useful way to think about forward propagation is as a computational graph. Each node represents an operation (matrix multiply, addition, activation function), and edges represent data flow.
For our simple network:
x → [W¹×] → [+b¹] → [ReLU] → a¹ → [W²×] → [+b²] → [sigmoid] → ŷ
This perspective becomes especially important when we talk about backpropagation (for MPS439 students). The graph structure tells us exactly how to compute gradients efficiently.
Now for the good news: you don't have to implement all this matrix math by hand! Keras is a high-level neural network library that handles all the complexity for us.
Keras is:
Think of Keras as the "easy mode" for neural networks. It lets you focus on the architecture and problem-solving rather than the implementation details.
Let's solve the XOR problem using Keras. You'll be amazed at how simple this is:
import numpy as np
from keras.models import Sequential
from keras.layers import Dense
import matplotlib.pyplot as plt
# Step 1: Create XOR dataset
X = np.array([[0, 0],
[0, 1],
[1, 0],
[1, 1]])
y = np.array([0, 1, 1, 0])
# Step 2: Build the neural network
model = Sequential([
Dense(4, input_dim=2, activation='relu'), # Hidden layer: 4 units, ReLU
Dense(1, activation='sigmoid') # Output layer: 1 unit, sigmoid
])
# Step 3: Compile the model
model.compile(optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy'])
# Step 4: Train the model
history = model.fit(X, y, epochs=1000, verbose=0)
# Step 5: Evaluate
predictions = model.predict(X)
print("XOR Predictions:")
for i in range(4):
print(f"Input: {X[i]} → Predicted: {predictions[i][0]:.4f}, True: {y[i]}")
Output:
XOR Predictions:
Input: [0 0] → Predicted: 0.0234, True: 0
Input: [0 1] → Predicted: 0.9812, True: 1
Input: [1 0] → Predicted: 0.9791, True: 1
Input: [1 1] → Predicted: 0.0187, True: 0
It works! The network successfully learned XOR. Predictions are very close to 0 or 1 as expected.
Let's break down what each part does:
1. Sequential([...]): Creates a feed-forward network where layers are stacked sequentially (one after another).
2. Dense(4, input_dim=2, activation='relu'):
Dense = fully connected layer (every input connects to every unit)4 = number of hidden unitsinput_dim=2 = we have 2 input features (only needed for first layer)activation='relu' = use ReLU activation function3. Dense(1, activation='sigmoid'):
4. compile(optimizer='adam', loss='binary_crossentropy'):
optimizer='adam' = how to update weights (Adam is a sophisticated version of gradient descent)loss='binary_crossentropy' = what to minimize (appropriate for binary classification)metrics=['accuracy'] = track accuracy during training5. fit(X, y, epochs=1000):
verbose=0 suppresses training output (use verbose=1 to see progress)6. predict(X): Make predictions for input data
That's it! About 10 lines of actual code to build, train, and use a neural network.
Let's visualize what the network learned:

This figure shows the decision boundary learned by our neural network. The background colors show what the network predicts for each point in the input space:
Notice how the decision boundary is non-linear (curved). The network successfully separates the XOR pattern, which logistic regression couldn't do!
Think about what we just accomplished:
This is the power of Keras. It abstracts away the complexity while still giving us full control over the architecture.
However: Understanding what's happening under the hood (sections 1-6) is crucial for:
We've seen how to build a network and make predictions (forward propagation). But how do networks learn? How do they figure out the right weights and biases?
The training process is conceptually the same as logistic regression:
The key difference: neural networks have many more parameters (sometimes millions!), organized in layers. But the principle is the same.
The loss function (also called cost function or objective function) measures how wrong our predictions are. It's what we're trying to minimize.
For binary classification (what we've been doing):
This is binary crossentropy (also called log loss). It's the same loss function we used in logistic regression! It heavily penalizes confident wrong predictions.
For regression problems:
This is mean squared error (MSE). Larger errors get penalized more heavily (squared term).
For multi-class classification (preview for later):
This is categorical crossentropy, which extends binary crossentropy to multiple classes.
In Keras, you just specify the appropriate loss:
loss='binary_crossentropy' for binary classificationloss='mse' for regressionloss='categorical_crossentropy' for multi-classImagine you're standing on a hill in thick fog. You can't see the bottom, but you can feel which direction is downhill. You take a step downhill, check again, take another step downhill, and repeat. Eventually, you reach (approximately) the bottom.
This is gradient descent:
Mathematically:
The gradients tell us which direction increases the loss. We go in the opposite direction (hence the minus sign) to decrease it.
Learning rate (): How big of steps to take
Epochs: One complete pass through the training data
Let's look at training curves to diagnose problems:

Top panel - Good Training:
Bottom panel - Overfitting:
Other common issues:
Underfitting:
Exploding/Vanishing Gradients:
Slow Convergence:
Here's a practical workflow:
optimizer='adam')Debugging checklist:
This is one of the most common questions! Here's practical guidance:
Start with 1 hidden layer. Seriously. For many problems, one hidden layer is sufficient. The universal approximation theorem tells us a network with one hidden layer can approximate any continuous function (given enough units).
Add a second layer if:
Go deeper (3+ layers) only if:
Why not always go deep?
Rule of thumb: Start shallow, go deeper only if you have clear evidence it helps.
For a single hidden layer, here's guidance:
Starting point: Between input size and output size
Adjust based on performance:
Practical ranges:
More hidden units:
Remember: Start small, increase if needed. It's easier to add capacity than to remove overfitting.
Neural networks are powerful, but they're not always the best choice. Here's when to use them:
Good candidates for neural networks:
Consider simpler models first:
The pragmatic approach:
Here's your practical workflow for using neural networks:
Step 1: Prepare Data
# Normalize features to similar scales
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
Why? Neural networks train better when features are on similar scales.
Step 2: Start Simple
model = Sequential([
Dense(8, input_dim=n_features, activation='relu'),
Dense(1, activation='sigmoid') # for binary classification
])
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
Step 3: Train and Monitor
history = model.fit(X_train_scaled, y_train,
validation_split=0.2, # Use 20% for validation
epochs=100,
verbose=1)
Step 4: Evaluate
test_loss, test_acc = model.evaluate(X_test_scaled, y_test)
print(f"Test accuracy: {test_acc:.3f}")
Step 5: Iterate
Pitfall 1: Forgetting to normalize features
# ❌ Bad: Features on very different scales
X = np.array([[1, 10000], [2, 20000], [3, 15000]])
# ✅ Good: Normalize first
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
Why it matters: Features with larger scales dominate the loss, making training unstable.
Pitfall 2: Too many epochs → Overfitting
# ❌ Bad: Training for 10,000 epochs without monitoring
model.fit(X, y, epochs=10000)
# ✅ Good: Monitor validation loss, use early stopping
from keras.callbacks import EarlyStopping
early_stop = EarlyStopping(monitor='val_loss', patience=10)
model.fit(X, y, epochs=1000, validation_split=0.2, callbacks=[early_stop])
Pitfall 3: Wrong activation on output
# ❌ Bad: ReLU on output for binary classification
Dense(1, activation='relu') # Output can be any positive number!
# ✅ Good: Sigmoid for binary classification
Dense(1, activation='sigmoid') # Output is probability between 0 and 1
Pitfall 4: Not splitting train/test
# ❌ Bad: Evaluate on training data
model.fit(X, y, epochs=100)
accuracy = model.evaluate(X, y) # This is training accuracy!
# ✅ Good: Evaluate on separate test set
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
model.fit(X_train, y_train, epochs=100)
accuracy = model.evaluate(X_test, y_test) # This is test accuracy
Pitfall 5: Using wrong loss function
# ❌ Bad: MSE for classification
model.compile(loss='mse') # Treats classes as numbers!
# ✅ Good: Binary crossentropy for binary classification
model.compile(loss='binary_crossentropy')
Note: This section is optional for MPS311 students. It provides mathematical details on how neural networks actually compute gradients. MPS311 students can skip to Section 12 (Summary).
Neural networks often have millions of parameters. Our XOR example had:
Real networks can have millions or even billions! How do we compute gradients for each of these efficiently?
Naive approach: Compute each gradient independently using finite differences:
This requires forward passes for parameters. With a million parameters, that's a million forward passes per gradient update! Completely impractical.
Backpropagation solves this by computing all gradients in just two passes: one forward, one backward. This is what makes training deep networks feasible.
The fundamental insight behind backpropagation is the chain rule from calculus.
Consider a simple composition: depends on , depends on . Then:
Neural networks are just deep compositions:
By repeatedly applying the chain rule, we can compute gradients for early layers by working backward through the network, reusing computations.
Key idea: Once we know , we can compute using local gradients. This is much more efficient than computing each gradient from scratch!
Let's derive backpropagation for a simple network: input → hidden layer → output .
Network equations:
Loss function (for one example, simplified MSE):
Goal: Compute
Output Layer Gradients:
Start with (we call this ):
Therefore:
Now we can get the weight and bias gradients for the output layer:
Hidden Layer Gradients:
Now we need (call this ):
The first term uses chain rule through :
The second term is the derivative of the activation:
where denotes element-wise multiplication.
Therefore:
Notice the pattern: the error from layer 2 () is "backpropagated" through the weights () to layer 1!
Finally:
Here's the backpropagation algorithm for a network with layers:
Forward Pass:
Backward Pass:
Compute output layer error:
For down to :
Compute gradients for all layers:
Update Weights:
Computational complexity:
The key efficiency comes from reusing computations. When computing , we reuse which we already computed. Each gradient is computed exactly once.
Memory-wise: We need to store activations from the forward pass (to use in backward pass), but this is much less than recomputing everything.
This efficiency is why we can train networks with millions or billions of parameters!
Here's a visual representation of the forward and backward passes:

In practice, frameworks like Keras and TensorFlow implement backpropagation automatically using automatic differentiation. You just define the forward pass (the network architecture), and the framework figures out the backward pass!
However, understanding backpropagation helps you:
Note: This section is optional for MPS311 students. It covers advanced training techniques that improve performance and prevent overfitting.
When training neural networks, especially with limited data, overfitting is a major concern. The network memorizes training examples instead of learning general patterns.
Regularization adds a penalty term to the loss function to discourage complex models:
L2 Regularization (Ridge):
L1 Regularization (Lasso):
In Keras:
from keras.regularizers import l2, l1
model = Sequential([
Dense(64, activation='relu', kernel_regularizer=l2(0.01)), # L2 with λ=0.01
Dense(32, activation='relu', kernel_regularizer=l1(0.01)), # L1 with λ=0.01
Dense(1, activation='sigmoid')
])
When to use:
Dropout is a powerful and simple regularization technique. During training, randomly set a fraction of neurons to zero.
How it works:
Why it works:
In Keras:
from keras.layers import Dropout
model = Sequential([
Dense(128, activation='relu'),
Dropout(0.5), # Drop 50% of neurons randomly during training
Dense(64, activation='relu'),
Dropout(0.3), # Drop 30% of neurons
Dense(1, activation='sigmoid')
])
Important: Dropout is only active during training. Keras automatically turns it off during prediction.
Typical values:
When to use:
Batch Normalization (BatchNorm) normalizes activations within each mini-batch. It stabilizes and speeds up training.
What it does:
For each mini-batch, normalize each feature to have mean 0 and variance 1:
Then scale and shift with learned parameters and :
Why it works:
In Keras:
from keras.layers import BatchNormalization
model = Sequential([
Dense(64),
BatchNormalization(), # Add after Dense, before activation
Activation('relu'),
Dense(32),
BatchNormalization(),
Activation('relu'),
Dense(1, activation='sigmoid')
])
Placement: Typically after Dense layer, before activation (though after activation also works).
When to use:
We've been using gradient descent conceptually. In practice, more sophisticated optimizers work much better.
1. SGD (Stochastic Gradient Descent):
Basic update rule:
With Momentum:
from keras.optimizers import SGD
model.compile(optimizer=SGD(learning_rate=0.01, momentum=0.9))
2. Adam (Adaptive Moment Estimation):
Combines momentum with adaptive learning rates for each parameter:
(momentum)
(adaptive learning rate)
from keras.optimizers import Adam
model.compile(optimizer=Adam(learning_rate=0.001)) # or just optimizer='adam'
3. RMSprop:
Similar to Adam but without momentum:
Comparison:
| Optimizer | Pros | Cons | When to Use |
|---|---|---|---|
| SGD | Simple, well-understood | Sensitive to learning rate, slow | When you have time to tune |
| SGD + Momentum | Faster than plain SGD | Still needs tuning | Research, well-studied problems |
| Adam | Works well out-of-box, fast convergence | Can generalize slightly worse | Default choice, general use |
| RMSprop | Good for non-stationary objectives | Less popular than Adam | RNNs, online learning |
Practical advice: Start with Adam. Only switch if you have specific reasons.
Let's implement a simple neural network from scratch to solidify understanding. We'll solve XOR again, but this time without Keras:
import numpy as np
# Activation functions
def sigmoid(z):
return 1 / (1 + np.exp(-z))
def sigmoid_derivative(z):
s = sigmoid(z)
return s * (1 - s)
def relu(z):
return np.maximum(0, z)
def relu_derivative(z):
return (z > 0).astype(float)
# XOR dataset
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y = np.array([[0], [1], [1], [0]])
# Initialize weights randomly
np.random.seed(42)
W1 = np.random.randn(2, 4) * 0.5 # 2 inputs -> 4 hidden units
b1 = np.zeros((1, 4))
W2 = np.random.randn(4, 1) * 0.5 # 4 hidden -> 1 output
b2 = np.zeros((1, 1))
# Hyperparameters
learning_rate = 0.5
epochs = 10000
# Training loop
for epoch in range(epochs):
# Forward pass
z1 = X.dot(W1) + b1
a1 = relu(z1)
z2 = a1.dot(W2) + b2
a2 = sigmoid(z2)
# Compute loss (MSE)
loss = np.mean((y - a2)**2)
# Backward pass
# Output layer
dL_da2 = -(y - a2) # derivative of MSE
da2_dz2 = sigmoid_derivative(z2)
delta2 = dL_da2 * da2_dz2
dL_dW2 = a1.T.dot(delta2)
dL_db2 = np.sum(delta2, axis=0, keepdims=True)
# Hidden layer
delta1 = delta2.dot(W2.T) * relu_derivative(z1)
dL_dW1 = X.T.dot(delta1)
dL_db1 = np.sum(delta1, axis=0, keepdims=True)
# Update weights
W2 -= learning_rate * dL_dW2
b2 -= learning_rate * dL_db2
W1 -= learning_rate * dL_dW1
b1 -= learning_rate * dL_db1
# Print progress
if epoch % 2000 == 0:
print(f"Epoch {epoch}, Loss: {loss:.4f}")
# Final predictions
print("\nFinal Predictions:")
for i in range(4):
print(f"Input: {X[i]} -> Prediction: {a2[i][0]:.4f}, True: {y[i][0]}")
Output:
Epoch 0, Loss: 0.2734
Epoch 2000, Loss: 0.0023
Epoch 4000, Loss: 0.0008
Epoch 6000, Loss: 0.0004
Epoch 8000, Loss: 0.0003
Final Predictions:
Input: [0 0] -> Prediction: 0.0156, True: 0
Input: [0 1] -> Prediction: 0.9847, True: 1
Input: [1 0] -> Prediction: 0.9851, True: 1
Input: [1 1] -> Prediction: 0.0148, True: 0
This ~40 line implementation demonstrates all the key concepts: forward propagation, loss computation, backpropagation, and weight updates!
Let's qualitatively compare the effects of different techniques:
Without regularization:
With L2 regularization (λ=0.01):
With Dropout (p=0.5):
With BatchNorm:
Practical advice:
Let's recap the journey we've taken today:
We started with logistic regression—a simple, elegant model that uses a weighted sum followed by a sigmoid activation. We saw that it creates a single decision boundary, perfect for linearly separable problems.
Then we discovered its fundamental limitation: the XOR problem. Four simple points that no single line can separate. This wasn't just an academic curiosity—it represented a whole class of problems where patterns are non-linear and complex.
Our solution was beautifully simple: stack multiple logistic regression units in layers! Let hidden units learn useful intermediate representations, then let the output unit make decisions based on these representations. By composing simple operations, we create arbitrarily complex functions.
We formalized this as neural network architecture: input layer → hidden layer(s) → output layer. Each layer transforms its input through weighted sums and non-linear activations.
We implemented a working neural network in about 10 lines of Keras code, solving the problem that stumped logistic regression. We trained it, watched it learn, and visualized its non-linear decision boundary.
We understood how networks learn through gradient descent and backpropagation, how to diagnose training issues through loss curves, and when neural networks are the right tool for the job.
Architecture Fundamentals:
Forward Propagation:
Activation Functions:
Training Process:
Keras Makes It Easy:
You can now:
Your workflow:
Remember:
You additionally understand:
You can:
Advanced workflow:
Neural networks are not magic. They're:
But through composition and scale, they become remarkably powerful:
The trade-offs:
Success requires:
"Start simple, add complexity as needed"
"More data > more complex model"
"Understand your training curves"
"When in doubt, try ReLU and Adam"
"Neural networks are tools, not solutions"
Congratulations! You've taken a major step in your machine learning journey. You now understand neural networks—from the fundamental intuition of stacking simple operations, through the mathematics of forward and backward propagation, to practical implementation with Keras.
The principles you learned today extend to much deeper and more sophisticated architectures: convolutional networks for images, recurrent networks for sequences, transformers for language. But they all build on the same foundations we covered here.
Keep experimenting, keep learning, and remember: even the most complex deep learning models are ultimately built from the simple building blocks we explored today.