Machine LearningClassification › Day 156

Hands-on lab — 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-boundaries when 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

  1. Derive and compute 2D linear decision boundary lines from model parameters.
  2. Calculate perpendicular signed distances from arbitrary feature vectors to the decision hyperplane.
  3. Transform 2D feature space with polynomial expansions to produce curved boundaries.
  4. Implement a One-vs-Rest (OvR) multiclass classifier and compute argmax class partitions.
  5. 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

  1. Implement a 2D decision boundary contour plotter with Matplotlib.
  2. Compute the volume of ambiguous prediction regions in One-vs-Rest classification.
  3. Compare OvR against Multinomial Softmax decision boundaries on a 3-class dataset.
  • 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 zero when calculating -w[0] / w[1].
  • Cause: The weight w[1] is exactly zero, meaning the decision boundary is a vertical line x1 = -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.0 or 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.