Hello! Welcome back to our journey of building machine learning models from scratch.
In our last session, you did an excellent job implementing two powerful kernel functions: the polynomial kernel and the Radial Basis Function (RBF) kernel. You now have the tools to compute similarity between data points in complex, high-dimensional spaces. However, these are currently just standalone functions.
Today, we'll bridge that gap. The goal of this lesson is to integrate these kernel functions directly into your SVM implementation, transforming it from a linear-only classifier into a powerful model capable of solving non-linear problems. We will shift our perspective from the primal formulation we used before to the more flexible dual formulation of the SVM, which is designed to work with kernels.
The Challenge: Why We Need a New Approach
Our previous SVM implementation solved the primal problem, where we directly optimized for the weight vector and bias . The decision function was simply . This approach is intuitive, but it's fundamentally tied to the original feature space of .
Kernels, on the other hand, operate by implicitly mapping our data into a higher-dimensional feature space . To work in this new space, we would need to find a weight vector in that same high-dimensional space. The "kernel trick" is that we can avoid this explicit, costly transformation by using a different mathematical formulation of the SVM problem: the dual problem.
The dual formulation rephrases the optimization problem so that it only depends on the dot products of data points, which we can then replace with our kernel function.

To get a quick and clear visual intuition for this process, let's watch a short video.
The Kernel Trick in Support Vector Machine (SVM)
The video 'The Kernel Trick in Support Vector Machine (SVM)' from the Visually Explained channel provides an excellent, concise overview of why we need kernels and how the trick works.
Watch the first two minutes (00:00 - 02:00). Focus on understanding that the SVM algorithm, in its dual form, only needs the inner product of the transformed data points, which is exactly what a kernel function provides.
The SVM Dual Formulation
Instead of optimizing for , the dual problem involves optimizing a set of variables called Lagrange multipliers, denoted by , one for each training sample.
The dual objective function we want to maximize is:
This is subject to the constraints .
The key things to notice are:
- The weight vector is gone!
- The data points and only appear inside the kernel function . This is where we plug in our polynomial or RBF kernel.
Once we find the optimal values, the prediction for a new data point is given by:
Data points for which are the support vectors. The decision boundary is determined entirely by them.
Implementation: Building a Kernelized SVM from Scratch
Now, let's get to the fun part: implementation. We will build a new SVM class that solves the dual problem using gradient ascent. This will be a major upgrade to your previous linear SVM.
We will follow the excellent guide from the article "Support Vector Machines for Beginners – Training Algorithms," which details this exact process.
Support Vector Machines for Beginners – Training Algorithms ...
This article by 'A Developer Diary' provides a complete from-scratch implementation of a kernelized SVM using the dual formulation and gradient ascent. We will use its structure and logic as our main reference.
Please read the sections 'Training Dual SVM using Kernel' and 'Python Implementation', focusing on the SVMDualProblem class. Pay close attention to: The gradient formula for alpha (Equation 14). How the fit method is structured: the vectorized gradient calculation, the update rule for alpha, and the clipping step. How the bias b is calculated after the training loop (Equation 23).
Inspired by that guide, let's structure our new KernelSVM class.
Step 1: __init__ and Kernel Functions
First, we'll set up the class to accept a kernel as a parameter. We'll also bring in the kernel functions you wrote in the last lesson. Since we're building a new class, let's put them inside for self-containment.
import numpy as np
class KernelSVM:
def __init__(self, kernel='rbf', C=1.0, gamma=0.1, degree=3, coef=1.0):
self.C = C # Regularization parameter
self.kernel_name = kernel
self.gamma = gamma # For RBF kernel
self.degree = degree # For polynomial kernel
self.coef = coef # For polynomial kernel
if self.kernel_name == 'rbf':
self.kernel = self._rbf_kernel
elif self.kernel_name == 'poly':
self.kernel = self._polynomial_kernel
else: # Default to linear kernel
self.kernel = self._linear_kernel
self.alphas = None
self.b = 0
self.X_train = None
self.y_train = None
def _linear_kernel(self, X1, X2):
return X1 @ X2.T
def _polynomial_kernel(self, X1, X2):
return (X1 @ X2.T + self.coef) ** self.degree
def _rbf_kernel(self, X1, X2):
# A pure NumPy implementation without SciPy
# This is for self-containment, though cdist is often faster
sq_dists = np.sum(X1**2, axis=1).reshape(-1, 1) + np.sum(X2**2, axis=1) - 2 * X1 @ X2.T
return np.exp(-self.gamma * sq_dists)
Step 2: The fit() Method
This is where the core logic of the dual formulation resides. We'll optimize the alphas using gradient ascent.
- Initialization: We initialize an
alphavector with one entry per training sample. - Kernel Matrix: We pre-compute a matrix
y_iy_jk_ij = np.outer(y, y) * self.kernel(X, X). This contains the term for all pairs and is constant, so we only compute it once. - Training Loop:
- Gradient: We compute the gradient for all
alphasin a vectorized way:gradient = 1 - y_iy_jk_ij.dot(self.alphas). - Update: We update
alphasusing gradient ascent:self.alphas += lr * gradient. - Clipping: We enforce the constraint by clipping the values.
- Gradient: We compute the gradient for all
- Bias Calculation: After the loop, we find the support vectors (where
0 < alpha < C) and use them to compute a stable value for the biasb.
# Add this method to your KernelSVM class
def fit(self, X, y, lr=1e-3, epochs=100):
n_samples, n_features = X.shape
self.X_train = X
self.y_train = y
# 1. Initialize alphas
self.alphas = np.zeros(n_samples)
self.b = 0
# 2. Pre-compute the kernel-related matrix
kernel_matrix = self.kernel(X, X)
y_iy_jk_ij = np.outer(y, y) * kernel_matrix
# 3. Training loop
for epoch in range(epochs):
# Vectorized gradient calculation
gradient = np.ones(n_samples) - y_iy_jk_ij.dot(self.alphas)
# Update alphas
self.alphas += lr * gradient
# Clipping to enforce 0 <= alpha <= C
self.alphas[self.alphas < 0] = 0
self.alphas[self.alphas > self.C] = self.C
# 4. Calculate bias b
# Find support vectors (more robust to use a small tolerance)
sv_indices = np.where((self.alphas > 1e-5) & (self.alphas < self.C - 1e-5))[0]
if len(sv_indices) > 0:
# Calculate b for each support vector and average
# b = y_k - sum_i(alpha_i * y_i * K(x_i, x_k))
b_k = y[sv_indices] - (self.alphas * y).dot(kernel_matrix[:, sv_indices])
self.b = np.mean(b_k)
else:
# Fallback if no SVs are found in the margin (can happen with some datasets/params)
print("Warning: Could not find support vectors in the margin to compute bias. Bias remains 0.")
self.b = 0
Test your understanding!
After training your KernelSVM on a dataset, you inspect the self.alphas vector and find that about 90% of its values are exactly 0. What does this tell you about the corresponding data points, and why is this a desirable property of SVMs?
Show answer
This means that 90% of the training data points are not support vectors. They lie correctly classified and outside the margin.
This is a desirable property because it leads to a sparse model. The decision boundary and all future predictions depend only on the support vectors (where alpha > 0). This makes the prediction phase very efficient, as we only need to compute kernels with a small subset of the original training data. It also gives us insight into which data points are the most critical for defining the class separation.
Step 3: The predict() Method
Prediction now relies on the learned alphas and the kernel function, not a weight vector w. For a new point , we compute the kernel between and all the training points , then weigh the results by .
# Add this method to your KernelSVM class
def predict(self, X):
# K(all_training_data, new_data)
kernel_matrix = self.kernel(self.X_train, X)
# decision_function = sum(alpha_i * y_i * K(x_i, z)) + b
decision_function = (self.alphas * self.y_train).dot(kernel_matrix) + self.b
return np.sign(decision_function)
Seeing it in Action
Now you have a complete KernelSVM! Let's test it on a classic non-linear dataset, like the "moons" dataset from scikit-learn.
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
# Generate data
X, y = make_moons(n_samples=200, noise=0.1, random_state=42)
y[y == 0] = -1 # SVMs typically use labels {-1, 1}
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# --- Train with RBF Kernel ---
svm_rbf = KernelSVM(kernel='rbf', C=1.0, gamma=10)
svm_rbf.fit(X_train, y_train, lr=1e-2, epochs=200)
# --- Plot decision boundary (helper function) ---
def plot_decision_boundary(svm, X, y):
x_min, x_max = X[:, 0].min() - 0.1, X[:, 0].max() + 0.1
y_min, y_max = X[:, 1].min() - 0.1, X[:, 1].max() + 0.1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.01),
np.arange(y_min, y_max, 0.01))
grid_points = np.c_[xx.ravel(), yy.ravel()]
Z = svm.predict(grid_points)
Z = Z.reshape(xx.shape)
plt.contourf(xx, yy, Z, alpha=0.4)
plt.scatter(X[:, 0], X[:, 1], c=y, s=20, edgecolors='k')
plt.title(f"SVM with {svm.kernel_name} kernel")
plt.show()
plot_decision_boundary(svm_rbf, X_train, y_train)
If you run this code, you should see a beautiful, non-linear boundary that successfully separates the two moon shapes! Try experimenting with a polynomial kernel as well.
Conclusion
Congratulations! You have successfully upgraded your SVM to handle non-linear data by implementing the dual formulation with kernels. This is a significant milestone and represents one of the most elegant ideas in classical machine learning.
Key Takeaways:
- Dual Formulation: To use kernels, we switched from the primal problem (optimizing ) to the dual problem (optimizing ).
- Optimization: We used gradient ascent to find the optimal
alphavalues, which represent the importance of each data point. - Sparsity: Most
alphasbecome zero, meaning the model relies only on a subset of data (the support vectors) for prediction, making it efficient. - Integration: You successfully modified your
fitandpredictmethods to use a generic kernel function, making your SVM versatile.
Preview of the Next Lesson:
We are now moving to a completely different family of algorithms. So far, our models (Linear/Logistic Regression, SVMs) have been geometric in nature, finding lines or planes to separate data. In the next module, we will begin our study of Decision Trees. These models make predictions by learning a hierarchy of simple if/else rules, creating a tree-like structure that is highly interpretable. Our first step will be to implement the logic for finding the best rule, or "split," in a dataset.