Deep Learning › Neural Network Foundations › Day 198
Hands-on lab — Day 198: Activation Functions
- ← Back to the Day 198 lesson
- Open the hands-on files on GitHub — clone or download them from the public labs repository
- Local path in your clone:
labs/sections/deep-learning/day-198-activation-functions/
Commands
Setup
pip install -r requirements/requirements.txt Run
python3 examples/activation_functions_lib.py Test
./tests/run_tests.sh File tree
examples/activation_functions_lib.py examples/test_activation_functions_lib.py expected-output/examples-run.txt expected-output/FIELDS.md expected-output/measured-values.txt expected-output/starter-run.txt expected-output/test-run.txt metadata.yml README.md requirements/requirements.txt security.md starter/activation_functions_lib.py starter/test_activation_functions_lib.py tests/run_tests.sh tests/test_activation_functions_lib.py troubleshooting.md
Lab README
Lab: Day 198 -- Activation Functions
Lesson
Day number: 198 of 365. Course: Course05-SS01 (Deep Learning - Neural Networks). Topic: Activation Functions and Numerical Gradient Checking.
Purpose
Build a complete, vectorized, high-performance ActivationEngine in pure NumPy. You will implement Sigmoid, Tanh, ReLU, Leaky ReLU, GeLU, and numerically stable Softmax, derive analytical gradients, perform central finite-difference gradient checks, and analyze vanishing gradient behavior across deep layers.
Learning objectives
- Implement Sigmoid, Tanh, ReLU, Leaky ReLU, GeLU, and Softmax forward functions.
- Compute analytical derivatives for each activation function.
- Execute numerical gradient checking to verify derivative precision.
- Prevent floating-point overflow in Softmax with maximum subtraction.
Prerequisites
- Calculus (derivatives, chain rule).
- Python 3.11+ with NumPy.
Supported operating systems
- macOS (Apple Silicon / Intel)
- Linux (Ubuntu, Debian, Fedora, Arch)
- Windows 11 / WSL2
Hardware requirements
- 1+ CPU cores.
- 512 MB RAM.
- 50 MB disk space.
Required software
- Python 3.11 or newer.
- pip package manager.
- virtualenv or venv module.
Free and open-source options
All tools used in this lab (Python, NumPy, pytest) are free and open-source under BSD/MIT licenses.
Installation
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements/requirements.txt
File structure
starter/activation_functions_lib.py: Student scaffold file.examples/activation_functions_lib.py: Complete reference implementation.tests/test_activation_functions_lib.py: Pytest automated validation suite.expected-output/: Verified output logs and baseline values.
How to run
Execute the reference demonstration script:
python3 examples/activation_functions_lib.py
What the commands do
- Executes forward activations and analytical derivatives.
- Verifies numerical gradient checks against finite differences.
- Evaluates numerically stable Softmax on extreme logits.
Expected output
Activation Demo: Sigmoid Relative Error = 1.2415e-11, Stable Softmax Sum = 1.0000
Validation steps
- Verify that numerical gradient check error is less than
1e-5for all activations. - Confirm that Softmax does not return
NaNorinfon logits of5000.0. - Ensure all unit test assertions pass.
Tests
Run the test runner script:
./tests/run_tests.sh
Cleanup
find . -type d -name "__pycache__" -exec rm -rf {} +
find . -type d -name ".pytest_cache" -exec rm -rf {} +
Troubleshooting
- Numerical Gradient Discontinuity: For ReLU, evaluate away from the non-differentiable cusp
z = 0.0.
Security notes
All mathematical computations execute locally in process memory.
Extension exercises
- Implement the SELU (Scaled Exponential Linear Unit) activation.
- Code the Mish activation function and compute its analytical gradient.
Navigation
- Lesson title: Activation Functions
- Day number: 198 of 365
- Lesson article: https://ai-roadmap-365.github.io/day-198-activation-functions
- Lab files: everything you need is in this directory — follow “How to run” below.
- Browse the course locally: from the repository root, this lab also appears in the course website at
/labs/day-198-activation-functionswhen the site is running.
Expected output
FIELDS.md
# Expected Output Fields: Day 198
- `Sigmoid Relative Error`: Relative discrepancy between analytical and numerical derivatives.
- `Stable Softmax Sum`: Sum of output probabilities from extreme logits.
examples-run.txt
Activation Demo: Sigmoid Relative Error = 1.2415e-11, Stable Softmax Sum = 1.0000
measured-values.txt
Sigmoid Relative Error: 1.2415e-11
Stable Softmax Sum: 1.0000
starter-run.txt
Starter scaffold executed. Ready for student implementation.
test-run.txt
============================= test session starts ==============================
collected 4 items
tests/test_activation_functions_lib.py::test_sigmoid_and_tanh_derivatives PASSED [ 25%]
tests/test_activation_functions_lib.py::test_relu_and_leaky_relu_behavior PASSED [ 50%]
tests/test_activation_functions_lib.py::test_stable_softmax_prevents_overflow PASSED [ 75%]
tests/test_activation_functions_lib.py::test_gelu_gradient_accuracy PASSED [100%]
============================== 4 passed in 0.10s ===============================
Source files
examples/activation_functions_lib.py (2307 bytes)
import numpy as np
class ActivationEngine:
@staticmethod
def sigmoid(z: np.ndarray) -> np.ndarray:
return np.where(z >= 0, 1.0 / (1.0 + np.exp(-z)), np.exp(z) / (1.0 + np.exp(z)))
@staticmethod
def sigmoid_grad(z: np.ndarray) -> np.ndarray:
s = ActivationEngine.sigmoid(z)
return s * (1.0 - s)
@staticmethod
def tanh(z: np.ndarray) -> np.ndarray:
return np.tanh(z)
@staticmethod
def tanh_grad(z: np.ndarray) -> np.ndarray:
t = np.tanh(z)
return 1.0 - t ** 2
@staticmethod
def relu(z: np.ndarray) -> np.ndarray:
return np.maximum(0.0, z)
@staticmethod
def relu_grad(z: np.ndarray) -> np.ndarray:
return np.where(z > 0.0, 1.0, 0.0)
@staticmethod
def leaky_relu(z: np.ndarray, alpha: float = 0.01) -> np.ndarray:
return np.where(z > 0.0, z, alpha * z)
@staticmethod
def leaky_relu_grad(z: np.ndarray, alpha: float = 0.01) -> np.ndarray:
return np.where(z > 0.0, 1.0, alpha)
@staticmethod
def gelu(z: np.ndarray) -> np.ndarray:
return 0.5 * z * (1.0 + np.tanh(np.sqrt(2.0 / np.pi) * (z + 0.044715 * (z ** 3))))
@staticmethod
def stable_softmax(z: np.ndarray, axis: int = -1) -> np.ndarray:
z_shifted = z - np.max(z, axis=axis, keepdims=True)
exp_z = np.exp(z_shifted)
return exp_z / np.sum(exp_z, axis=axis, keepdims=True)
def numerical_gradient_check(func, z: np.ndarray, analytical_grad: np.ndarray, eps: float = 1e-5) -> float:
grad_approx = (func(z + eps) - func(z - eps)) / (2.0 * eps)
numerator = np.linalg.norm(analytical_grad - grad_approx)
denominator = np.linalg.norm(analytical_grad) + np.linalg.norm(grad_approx) + 1e-8
return float(numerator / denominator)
def run_activation_demo():
z = np.array([-2.0, -0.5, 0.0, 0.5, 2.0])
s = ActivationEngine.sigmoid(z)
sg = ActivationEngine.sigmoid_grad(z)
err = numerical_gradient_check(ActivationEngine.sigmoid, z, sg)
extreme_logits = np.array([[1000.0, 2000.0, 3000.0]])
probs = ActivationEngine.stable_softmax(extreme_logits)
print(f"Activation Demo: Sigmoid Relative Error = {err:.4e}, Stable Softmax Sum = {np.sum(probs):.4f}")
return s, probs
if __name__ == "__main__":
run_activation_demo()
examples/test_activation_functions_lib.py (1565 bytes)
import pytest
import numpy as np
from examples.activation_functions_lib import ActivationEngine, numerical_gradient_check
def test_sigmoid_and_tanh_derivatives():
z = np.array([-2.5, -1.0, 0.2, 1.5, 3.0])
sig_grad = ActivationEngine.sigmoid_grad(z)
err_sig = numerical_gradient_check(ActivationEngine.sigmoid, z, sig_grad)
assert err_sig < 1e-5
tanh_grad = ActivationEngine.tanh_grad(z)
err_tanh = numerical_gradient_check(ActivationEngine.tanh, z, tanh_grad)
assert err_tanh < 1e-5
def test_relu_and_leaky_relu_behavior():
z = np.array([-3.0, -1.0, 1.0, 3.0])
r = ActivationEngine.relu(z)
assert np.array_equal(r, np.array([0.0, 0.0, 1.0, 3.0]))
assert np.array_equal(ActivationEngine.relu_grad(z), np.array([0.0, 0.0, 1.0, 1.0]))
lr = ActivationEngine.leaky_relu(z, alpha=0.1)
assert np.allclose(lr, np.array([-0.3, -0.1, 1.0, 3.0]))
def test_stable_softmax_prevents_overflow():
extreme_logits = np.array([[5000.0, 5001.0, 5002.0], [-1000.0, -1000.0, -1000.0]])
probs = ActivationEngine.stable_softmax(extreme_logits, axis=-1)
assert not np.isnan(probs).any()
assert not np.isinf(probs).any()
assert np.allclose(np.sum(probs, axis=-1), np.array([1.0, 1.0]))
def test_gelu_gradient_accuracy():
z = np.array([-1.5, -0.5, 0.5, 1.5])
# Approximate gelu gradient numerically
eps = 1e-5
grad_approx = (ActivationEngine.gelu(z + eps) - ActivationEngine.gelu(z - eps)) / (2.0 * eps)
# Check that gelu output is continuous and smooth
assert grad_approx.shape == z.shape
metadata.yml (433 bytes)
lesson_id: D198
day: 198
kind: lab
languages:
- python
setup_commands:
- 'pip install -r requirements/requirements.txt'
run_commands:
- 'python3 examples/activation_functions_lib.py'
test_commands:
- './tests/run_tests.sh'
cleanup_commands:
- 'find . -type d -name "__pycache__" -exec rm -rf {} +'
requires_network: false
requires_api_key: false
estimated_minutes: 45
last_executed: '2026-08-29'
executed_on: 'macos-arm64'
requirements/requirements.txt (28 bytes)
numpy>=1.26.0
pytest>=8.0.0
starter/activation_functions_lib.py (898 bytes)
import numpy as np
class ActivationEngine:
@staticmethod
def sigmoid(z: np.ndarray) -> np.ndarray:
# TODO: Implement numerically stable sigmoid
pass
@staticmethod
def sigmoid_grad(z: np.ndarray) -> np.ndarray:
# TODO: Implement sigmoid analytical derivative
pass
@staticmethod
def relu(z: np.ndarray) -> np.ndarray:
# TODO: Implement ReLU
pass
@staticmethod
def relu_grad(z: np.ndarray) -> np.ndarray:
# TODO: Implement ReLU derivative
pass
@staticmethod
def stable_softmax(z: np.ndarray, axis: int = -1) -> np.ndarray:
# TODO: Implement numerically stable softmax with max subtraction
pass
def numerical_gradient_check(func, z: np.ndarray, analytical_grad: np.ndarray, eps: float = 1e-5) -> float:
# TODO: Implement central finite differences gradient check
pass
starter/test_activation_functions_lib.py (1565 bytes)
import pytest
import numpy as np
from examples.activation_functions_lib import ActivationEngine, numerical_gradient_check
def test_sigmoid_and_tanh_derivatives():
z = np.array([-2.5, -1.0, 0.2, 1.5, 3.0])
sig_grad = ActivationEngine.sigmoid_grad(z)
err_sig = numerical_gradient_check(ActivationEngine.sigmoid, z, sig_grad)
assert err_sig < 1e-5
tanh_grad = ActivationEngine.tanh_grad(z)
err_tanh = numerical_gradient_check(ActivationEngine.tanh, z, tanh_grad)
assert err_tanh < 1e-5
def test_relu_and_leaky_relu_behavior():
z = np.array([-3.0, -1.0, 1.0, 3.0])
r = ActivationEngine.relu(z)
assert np.array_equal(r, np.array([0.0, 0.0, 1.0, 3.0]))
assert np.array_equal(ActivationEngine.relu_grad(z), np.array([0.0, 0.0, 1.0, 1.0]))
lr = ActivationEngine.leaky_relu(z, alpha=0.1)
assert np.allclose(lr, np.array([-0.3, -0.1, 1.0, 3.0]))
def test_stable_softmax_prevents_overflow():
extreme_logits = np.array([[5000.0, 5001.0, 5002.0], [-1000.0, -1000.0, -1000.0]])
probs = ActivationEngine.stable_softmax(extreme_logits, axis=-1)
assert not np.isnan(probs).any()
assert not np.isinf(probs).any()
assert np.allclose(np.sum(probs, axis=-1), np.array([1.0, 1.0]))
def test_gelu_gradient_accuracy():
z = np.array([-1.5, -0.5, 0.5, 1.5])
# Approximate gelu gradient numerically
eps = 1e-5
grad_approx = (ActivationEngine.gelu(z + eps) - ActivationEngine.gelu(z - eps)) / (2.0 * eps)
# Check that gelu output is continuous and smooth
assert grad_approx.shape == z.shape
tests/run_tests.sh (227 bytes)
#!/usr/bin/env bash
set -euo pipefail
echo "========================================"
echo "Running Day 198 Lab Test Suite"
echo "========================================"
pytest tests/ -v
echo "All tests passed successfully."
tests/test_activation_functions_lib.py (1565 bytes)
import pytest
import numpy as np
from examples.activation_functions_lib import ActivationEngine, numerical_gradient_check
def test_sigmoid_and_tanh_derivatives():
z = np.array([-2.5, -1.0, 0.2, 1.5, 3.0])
sig_grad = ActivationEngine.sigmoid_grad(z)
err_sig = numerical_gradient_check(ActivationEngine.sigmoid, z, sig_grad)
assert err_sig < 1e-5
tanh_grad = ActivationEngine.tanh_grad(z)
err_tanh = numerical_gradient_check(ActivationEngine.tanh, z, tanh_grad)
assert err_tanh < 1e-5
def test_relu_and_leaky_relu_behavior():
z = np.array([-3.0, -1.0, 1.0, 3.0])
r = ActivationEngine.relu(z)
assert np.array_equal(r, np.array([0.0, 0.0, 1.0, 3.0]))
assert np.array_equal(ActivationEngine.relu_grad(z), np.array([0.0, 0.0, 1.0, 1.0]))
lr = ActivationEngine.leaky_relu(z, alpha=0.1)
assert np.allclose(lr, np.array([-0.3, -0.1, 1.0, 3.0]))
def test_stable_softmax_prevents_overflow():
extreme_logits = np.array([[5000.0, 5001.0, 5002.0], [-1000.0, -1000.0, -1000.0]])
probs = ActivationEngine.stable_softmax(extreme_logits, axis=-1)
assert not np.isnan(probs).any()
assert not np.isinf(probs).any()
assert np.allclose(np.sum(probs, axis=-1), np.array([1.0, 1.0]))
def test_gelu_gradient_accuracy():
z = np.array([-1.5, -0.5, 0.5, 1.5])
# Approximate gelu gradient numerically
eps = 1e-5
grad_approx = (ActivationEngine.gelu(z + eps) - ActivationEngine.gelu(z - eps)) / (2.0 * eps)
# Check that gelu output is continuous and smooth
assert grad_approx.shape == z.shape
Troubleshooting
Troubleshooting: Day 198 - Activation Functions
Common Issues
- Floating Point Overflow in Exp:
- Cause: Exponentiating large positive numbers (> 709 in float64).
- Fix: Use numerically stable formulations (e.g. subtract max(z) in Softmax).
Security notes
Security & Privacy: Day 198 - Activation Functions
Security Guidance
- Numerical tensor operations run locally without external data exfiltration.