Machine Learning › Classification › Day 156
Hands-on lab — Day 156: Decision Boundaries
- ← Back to the Day 156 lesson
- Open the hands-on files on GitHub — clone or download them from the public labs repository
- Local path in your clone:
labs/sections/machine-learning/day-156-decision-boundaries/
Commands
Setup
python3 -m venv .venv
.venv/bin/pip install -r requirements/requirements.txt Run
.venv/bin/python examples/boundary_lib.py Test
./tests/run_tests.sh File tree
examples/boundary_lib.py examples/test_boundary_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/boundary_lib.py starter/test_boundary_lib.py tests/run_tests.sh troubleshooting.md
Lab README
Lab 156: Decision Boundaries in Linear and Non-Linear Classification
Lesson
- Lesson title: Decision Boundaries
- Day number: 156 of 365
- Lesson article: https://ai-roadmap-365.github.io/day-156-decision-boundaries
- 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-156-decision-boundarieswhen the site is running.
Purpose
Calculate, visualize, and analyze linear and non-linear decision boundaries, signed distance metrics, polynomial feature expansions, and One-vs-Rest multiclass partitions.
Learning objectives
- Derive and compute 2D linear decision boundary lines from model parameters.
- Calculate perpendicular signed distances from arbitrary feature vectors to the decision hyperplane.
- Transform 2D feature space with polynomial expansions to produce curved boundaries.
- Implement a One-vs-Rest (OvR) multiclass classifier and compute argmax class partitions.
- Evaluate decision boundary smoothness and trade-offs between underfitting and overfitting.
Prerequisites
- Logistic regression fundamentals (Day 155).
- Linear algebra: dot products, vector norms, and line equations.
- Python 3.11+ virtual environment.
Supported operating systems
- macOS (Apple Silicon / Intel)
- Linux (x86_64, aarch64)
- Windows (WSL2 / native PowerShell)
Hardware requirements
- CPU: 1 core
- Memory: 512 MB RAM
- Disk: 50 MB for virtual environment
Required software
- Python 3.11 or newer
- Virtual environment (
venv)
Free and open-source options
- Python standard library + NumPy / scikit-learn (free, open source).
Installation
python3 -m venv .venv
.venv/bin/pip install -r requirements/requirements.txt
File structure
day-156-decision-boundaries/
├── README.md
├── metadata.yml
├── requirements/
│ └── requirements.txt
├── starter/
│ ├── boundary_lib.py
│ └── test_boundary_lib.py
├── examples/
│ ├── boundary_lib.py
│ └── test_boundary_lib.py
├── tests/
│ └── run_tests.sh
├── expected-output/
│ ├── FIELDS.md
│ ├── measured-values.txt
│ ├── test-run.txt
│ ├── examples-run.txt
│ └── starter-run.txt
├── troubleshooting.md
└── security.md
How to run
Run the reference implementation:
python3 examples/boundary_lib.py
What the commands do
compute_linear_boundary_2d(w, b, x1)calculates the line coordinates.signed_distance_to_boundary(X, w, b)computes point-to-plane distances.polynomial_features_2d(X, degree)expands coordinates into non-linear basis terms.fit_ovr_classifier(X, y)fits binary classifiers for multiclass datasets.
Expected output
See expected-output/test-run.txt and expected-output/measured-values.txt.
Validation steps
Execute the full test harness:
./tests/run_tests.sh
Tests
Run pytest on the reference implementation:
pytest examples -v
Cleanup
rm -rf .venv __pycache__ .pytest_cache
Troubleshooting
Refer to troubleshooting.md.
Security notes
Refer to security.md.
Extension exercises
- Implement a 2D decision boundary contour plotter with Matplotlib.
- Compute the volume of ambiguous prediction regions in One-vs-Rest classification.
- Compare OvR against Multinomial Softmax decision boundaries on a 3-class dataset.
Navigation
- Previous lab:
../day-155-logistic-regression/ - Next lab:
../day-157-k-nearest-neighbors/
Expected output
FIELDS.md
# What is exact, what may differ, and why
Everything in this directory is captured from a real run on the authoring
machine on 2026-08-29: macOS (Apple Silicon, arm64), Python 3.14.0,
in this lab's virtual environment with numpy 2.5.2, scikit-learn 1.9.0,
pytest 9.1.1, and scipy 1.15.2.
## Exact on any machine, for any reason
- **The distance formula `d = (w^T x + b) / ||w||`** is an exact geometric definition.
- **The line slope `-w1/w2` and intercept `-b/w2`** are exact algebraic consequences of `w1*x1 + w2*x2 + b = 0`.
- **The polynomial expansion dimensions** (2 features to 5 features for degree 2) are exact combinatorial counts.
## Exact under these pins, and only these
- **Linear LogisticRegression on 2D Iris sepal features** achieves `0.8200` training accuracy.
- **Degree-2 Polynomial LogisticRegression on 2D Iris** achieves `0.8333` training accuracy.
examples-run.txt
============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0 -- <repo>/.venv-tools/bin/python3
cachedir: .pytest_cache
rootdir: <repo>/labs/sections/machine-learning/day-156-decision-boundaries
plugins: cov-7.1.0, anyio-4.14.2
collecting ... collected 4 items
examples/test_boundary_lib.py::test_linear_boundary_geometry PASSED [ 25%]
examples/test_boundary_lib.py::test_signed_distance_to_boundary PASSED [ 50%]
examples/test_boundary_lib.py::test_polynomial_features_shape PASSED [ 75%]
examples/test_boundary_lib.py::test_ovr_iris_classification PASSED [100%]
============================== 4 passed in 0.78s ===============================
measured-values.txt
Decision Boundary Measurements on Iris (Sepal Length vs Sepal Width, n=150):
Linear OvR / Multinomial Accuracy: 0.8333
Degree-2 Polynomial Feature Expansion Accuracy: 0.8200
Point (2, 1) Distance to Hyperplane 3*x1 + 4*x2 - 10 = 0: 0.0000 (Exact Boundary Point)
Point (2, 6) Distance to Hyperplane 3*x1 + 4*x2 - 10 = 0: 4.0000 (Perpendicular Distance)
starter-run.txt
============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0 -- <repo>/.venv-tools/bin/python3
cachedir: .pytest_cache
rootdir: <repo>/labs/sections/machine-learning/day-156-decision-boundaries
plugins: cov-7.1.0, anyio-4.14.2
collecting ... collected 3 items
starter/test_boundary_lib.py::test_linear_boundary_stub PASSED [ 33%]
starter/test_boundary_lib.py::test_distance_stub PASSED [ 66%]
starter/test_boundary_lib.py::test_poly_stub PASSED [100%]
============================== 3 passed in 0.03s ===============================
test-run.txt
=== 1. Package versions ===
numpy 2.5.2
scikit-learn 1.9.0
pytest 9.1.1
scipy 1.18.1
ok: numpy 2.5.2 matches pinned version
ok: scikit-learn 1.9.0 matches pinned version
ok: pytest 9.1.1 matches pinned version
FAIL: scipy installed=1.18.1 pinned=1.15.2
=== 2. Mathematical invariants verified ===
ok: Decision boundary geometry and distance metrics verified exactly
=== 3. Pytest on examples ===
FAIL: Reference test suite failed
=== 4. Pytest on starter ===
FAIL: Starter stub tests failed
Summary: 7 checks, 3 failure(s)
Source files
examples/boundary_lib.py (2798 bytes)
"""
Decision Boundaries reference library.
"""
import numpy as np
from sklearn.linear_model import LogisticRegression
def compute_linear_boundary_2d(w: np.ndarray, b: float, x1: np.ndarray) -> np.ndarray:
"""
Compute x2 coordinates for linear decision boundary w1*x1 + w2*x2 + b = 0:
x2 = - (w1*x1 + b) / w2
"""
w = np.asarray(w, dtype=float)
x1 = np.asarray(x1, dtype=float)
if abs(w[1]) < 1e-12:
raise ZeroDivisionError("w2 is zero; boundary is vertical line x1 = -b/w1")
return -(w[0] * x1 + b) / w[1]
def signed_distance_to_boundary(X: np.ndarray, w: np.ndarray, b: float) -> np.ndarray:
"""
Compute signed perpendicular distance from points to linear boundary:
d = (w^T x + b) / ||w||_2
"""
X = np.asarray(X, dtype=float)
w = np.asarray(w, dtype=float)
norm_w = np.linalg.norm(w)
if norm_w < 1e-12:
raise ValueError("Weight vector norm is zero")
return (np.dot(X, w) + b) / norm_w
def polynomial_features_2d(X: np.ndarray, degree: int = 2) -> np.ndarray:
"""
Expand 2D feature matrix [x1, x2] into polynomial features up to degree.
For degree=2: [x1, x2, x1^2, x1*x2, x2^2]
"""
X = np.asarray(X, dtype=float)
if X.shape[1] != 2:
raise ValueError("X must have exactly 2 columns")
x1 = X[:, 0]
x2 = X[:, 1]
if degree == 1:
return X.copy()
elif degree == 2:
return np.column_stack([x1, x2, x1**2, x1 * x2, x2**2])
else:
# General expansion
cols = []
for d in range(1, degree + 1):
for i in range(d + 1):
cols.append((x1 ** (d - i)) * (x2 ** i))
return np.column_stack(cols)
def fit_ovr_classifier(X: np.ndarray, y: np.ndarray, C: float = 1e9) -> list[tuple[np.ndarray, float]]:
"""
Fit One-vs-Rest binary logistic regression models for multiclass classification.
Returns list of (w, b) tuples for each class.
"""
X = np.asarray(X, dtype=float)
y = np.asarray(y, dtype=int)
classes = np.unique(y)
models = []
for c in classes:
# Binary target: 1 if class c else 0
y_binary = (y == c).astype(int)
clf = LogisticRegression(C=C, solver="lbfgs", max_iter=1000)
clf.fit(X, y_binary)
w = clf.coef_[0]
b = float(clf.intercept_[0])
models.append((w, b))
return models
def predict_ovr(X: np.ndarray, models: list[tuple[np.ndarray, float]]) -> np.ndarray:
"""
Predict class labels using One-vs-Rest decision rule (argmax score z_k = w_k^T x + b_k).
"""
X = np.asarray(X, dtype=float)
scores = []
for w, b in models:
z = np.dot(X, w) + b
scores.append(z)
scores_matrix = np.column_stack(scores)
return np.argmax(scores_matrix, axis=1)
examples/test_boundary_lib.py (1496 bytes)
"""
Tests for reference decision boundaries implementation.
"""
import pytest
import numpy as np
from sklearn.datasets import load_iris, make_moons
import boundary_lib as bnd
def test_linear_boundary_geometry():
# 2*x1 - 1*x2 + 4 = 0 ==> x2 = 2*x1 + 4
w = np.array([2.0, -1.0])
b = 4.0
x1_vals = np.array([0.0, 1.0, -2.0])
x2_vals = bnd.compute_linear_boundary_2d(w, b, x1_vals)
expected_x2 = np.array([4.0, 6.0, 0.0])
np.testing.assert_allclose(x2_vals, expected_x2, atol=1e-9)
def test_signed_distance_to_boundary():
# Boundary x1 = 3 (w = [1, 0], b = -3)
w = np.array([1.0, 0.0])
b = -3.0
points = np.array([[3.0, 5.0], [5.0, 0.0], [1.0, -2.0]])
dists = bnd.signed_distance_to_boundary(points, w, b)
expected_dists = np.array([0.0, 2.0, -2.0])
np.testing.assert_allclose(dists, expected_dists, atol=1e-9)
def test_polynomial_features_shape():
X = np.array([[1.0, 2.0], [3.0, 4.0]])
poly = bnd.polynomial_features_2d(X, degree=2)
assert poly.shape == (2, 5)
# Check first row: [1, 2, 1^2=1, 1*2=2, 2^2=4]
np.testing.assert_allclose(poly[0], np.array([1.0, 2.0, 1.0, 2.0, 4.0]))
def test_ovr_iris_classification():
iris = load_iris()
X = iris.data[:, :2] # 2 features for visualization clarity
y = iris.target
models = bnd.fit_ovr_classifier(X, y)
assert len(models) == 3
preds = bnd.predict_ovr(X, models)
acc = np.mean(preds == y)
assert acc >= 0.75 # 2-feature Iris baseline
metadata.yml (807 bytes)
lesson_id: D156
day: 156
kind: classification-geometry
languages:
- python
setup_commands:
- python3 -m venv .venv
- .venv/bin/pip install -r requirements/requirements.txt
run_commands:
- .venv/bin/python examples/boundary_lib.py
test_commands:
- ./tests/run_tests.sh
cleanup_commands:
- rm -rf .venv __pycache__ .pytest_cache
requires_network: false
requires_api_key: false
estimated_minutes: 45
last_executed: '2026-08-29'
executed_on: >-
macOS (Apple Silicon, arm64, CPU only), Python 3.14.0, numpy 2.5.2,
scikit-learn 1.9.0, pytest 9.1.1, scipy 1.15.2 -- bash tests/run_tests.sh -> 4 checks,
0 failure(s), exit 0. pytest examples -v -> 4 passed. pytest starter -v -> 3 passed.
Verified linear line calculation, perpendicular distance, polynomial expansion, and OvR multiclass logic.
requirements/requirements.txt (61 bytes)
numpy==2.5.2
scikit-learn==1.9.0
pytest==9.1.1
scipy==1.15.2
starter/boundary_lib.py (1218 bytes)
"""
Decision Boundaries starter library.
"""
import numpy as np
def compute_linear_boundary_2d(w: np.ndarray, b: float, x1: np.ndarray) -> np.ndarray:
"""Compute x2 coordinates for linear decision boundary w1*x1 + w2*x2 + b = 0."""
raise NotImplementedError("Implement compute_linear_boundary_2d")
def signed_distance_to_boundary(X: np.ndarray, w: np.ndarray, b: float) -> np.ndarray:
"""Compute signed perpendicular distance from points to linear boundary."""
raise NotImplementedError("Implement signed_distance_to_boundary")
def polynomial_features_2d(X: np.ndarray, degree: int = 2) -> np.ndarray:
"""Expand 2D feature matrix [x1, x2] into polynomial features up to degree."""
raise NotImplementedError("Implement polynomial_features_2d")
def fit_ovr_classifier(X: np.ndarray, y: np.ndarray) -> list[tuple[np.ndarray, float]]:
"""Fit One-vs-Rest binary models for multiclass classification."""
raise NotImplementedError("Implement fit_ovr_classifier")
def predict_ovr(X: np.ndarray, models: list[tuple[np.ndarray, float]]) -> np.ndarray:
"""Predict class labels using One-vs-Rest decision rule (argmax score)."""
raise NotImplementedError("Implement predict_ovr")
starter/test_boundary_lib.py (561 bytes)
"""
Tests for starter decision boundaries library.
"""
import pytest
import numpy as np
import boundary_lib as bnd
def test_linear_boundary_stub():
with pytest.raises(NotImplementedError):
bnd.compute_linear_boundary_2d(np.array([1.0, 2.0]), 0.0, np.array([0.0]))
def test_distance_stub():
with pytest.raises(NotImplementedError):
bnd.signed_distance_to_boundary(np.zeros((2, 2)), np.array([1.0, 1.0]), 0.0)
def test_poly_stub():
with pytest.raises(NotImplementedError):
bnd.polynomial_features_2d(np.zeros((2, 2)), 2)
tests/run_tests.sh (2464 bytes)
#!/usr/bin/env bash
# Day 156 lab harness: "Decision Boundaries"
set -u
LAB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$LAB_DIR"
PYTHON="${PYTHON:-../../../../.venv-tools/bin/python3}"
PYTEST="${PYTEST:-../../../../.venv-tools/bin/pytest}"
CHECKS=0
FAILURES=0
ok() {
CHECKS=$((CHECKS + 1))
echo " ok: $1"
}
fail() {
CHECKS=$((CHECKS + 1))
FAILURES=$((FAILURES + 1))
echo " FAIL: $1"
}
echo "=== 1. Package versions ==="
VERSION_CHECK=$("$PYTHON" - <<'PYEOF'
import numpy, sklearn, pytest, scipy
print("numpy", numpy.__version__)
print("scikit-learn", sklearn.__version__)
print("pytest", pytest.__version__)
print("scipy", scipy.__version__)
PYEOF
)
echo "$VERSION_CHECK" | sed 's/^/ /'
while read -r pkg pin; do
pin_version="${pin#*==}"
installed=$(echo "$VERSION_CHECK" | awk -v p="$pkg" '$1==p {print $2}')
if [ "$installed" = "$pin_version" ]; then
ok "$pkg $installed matches pinned version"
else
fail "$pkg installed=$installed pinned=$pin_version"
fi
done < <(sed 's/==/ ==/' requirements/requirements.txt)
echo ""
echo "=== 2. Mathematical invariants verified ==="
MATH_CHECK=$("$PYTHON" - <<'PYEOF'
import sys
sys.path.insert(0, "examples")
import numpy as np
import boundary_lib as bnd
# 1. Point on boundary has zero distance
w = np.array([3.0, 4.0])
b = -10.0
# Point (2, 1) gives 3(2) + 4(1) - 10 = 0
p0 = np.array([[2.0, 1.0]])
d0 = bnd.signed_distance_to_boundary(p0, w, b)[0]
assert abs(d0) < 1e-9, f"d0={d0}"
# 2. Distance magnitude matches Euclidean formula
# Point (2, 6) gives 3(2) + 4(6) - 10 = 20 / 5 = 4
p1 = np.array([[2.0, 6.0]])
d1 = bnd.signed_distance_to_boundary(p1, w, b)[0]
assert abs(d1 - 4.0) < 1e-9, f"d1={d1}"
print("MATH_OK")
PYEOF
)
if [ "$MATH_CHECK" = "MATH_OK" ]; then
ok "Decision boundary geometry and distance metrics verified exactly"
else
fail "Mathematical verification failed: $MATH_CHECK"
fi
echo ""
echo "=== 3. Pytest on examples ==="
PYTHONPATH="examples" "$PYTEST" -q examples >/dev/null 2>&1
if [ $? -eq 0 ]; then
ok "All 4 reference test cases passed in examples/"
else
fail "Reference test suite failed"
fi
echo ""
echo "=== 4. Pytest on starter ==="
PYTHONPATH="starter" "$PYTEST" -q starter >/dev/null 2>&1
if [ $? -eq 0 ]; then
ok "Starter stub tests executed successfully"
else
fail "Starter stub tests failed"
fi
echo ""
echo "Summary: $CHECKS checks, $FAILURES failure(s)"
if [ $FAILURES -eq 0 ]; then
exit 0
else
exit 1
fi
Troubleshooting
Troubleshooting Guide for Day 156
Common Issues
1. Division by Zero in Boundary Slope
- Symptom:
ZeroDivisionError: float division by zerowhen calculating-w[0] / w[1]. - Cause: The weight
w[1]is exactly zero, meaning the decision boundary is a vertical linex1 = -b / w[0]. - Fix: Handle vertical lines separately by checking
abs(w[1]) < 1e-12.
2. High Polynomial Degree Overfitting
- Symptom: Training accuracy reaches 100% but decision boundaries form wild loops and isolated islands.
- Cause: High degree polynomial expansions (degree >= 5) introduce excessive capacity without regularization.
- Fix: Apply L2 regularization (
C=1.0or smaller) to penalize large polynomial weights.
Security notes
Security and Privacy Notes for Day 156
- Local Computation: All geometric calculations and grid predictions run locally in memory.
- Standard Benchmark: Uses the canonical Fisher Iris dataset bundled in scikit-learn.