Neroml / Templates /svm.html
deedrop1140's picture
Upload 69 files
6491927 verified
{% extends "layout.html" %}
{% block content %}
<style>
/* Custom styles for the SVM visualization page */
/* The body styles are moved to svm-page-wrapper */
.svm-page-wrapper {
font-family: "Inter", sans-serif;
background-color: #f0f4f8; /* Light background */
display: flex;
justify-content: center;
/* Align items to start, not center, to prevent content being pushed up */
align-items: flex-start; /* Changed from center to flex-start */
min-height: 100vh; /* Ensure it takes full viewport height */
padding: 20px;
box-sizing: border-box;
width: 100%; /* Ensure it takes full width of its parent (.main) */
overflow-y: auto; /* Allow scrolling if content overflows vertically */
}
.container {
max-width: 1000px; /* Increased max-width for better layout */
width: 100%; /* Ensure it respects max-width and is responsive */
background-color: #ffffff;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
/* No fixed height or min-height here, let content dictate height */
}
h1, h2, h3, h4 {
color: #2c3e50;
}
.info-icon {
cursor: help;
margin-left: 5px;
color: #6B7280; /* gray-500 */
position: relative;
display: inline-block;
}
.tooltip {
visibility: hidden;
width: 250px;
background-color: #333;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 8px 10px;
position: absolute;
z-index: 10;
bottom: 125%;
left: 50%;
margin-left: -125px;
opacity: 0;
transition: opacity 0.3s;
font-size: 0.85rem;
line-height: 1.4;
}
.info-icon:hover .tooltip {
visibility: visible;
opacity: 1;
}
.tooltip::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: #333 transparent transparent transparent;
}
.highlight-blue { color: #2563EB; font-weight: 600; }
.highlight-red { color: #DC2626; font-weight: 600; }
.highlight-green { color: #16A34A; font-weight: 600; }
.highlight-purple { color: #9333EA; font-weight: 600; }
.highlight-bold { font-weight: 600; }
.flow-box {
background-color: #F3F4F6;
border-radius: 0.5rem;
padding: 1.5rem;
text-align: center;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
min-height: 120px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.flow-arrow {
font-size: 2.5rem;
color: #9CA3AF;
margin: 0 1rem;
display: flex;
align-items: center;
justify-content: center;
}
.math {
font-size: 1.1em;
display: block;
margin-top: 1em;
margin-bottom: 1em;
overflow-x: auto;
padding: 0.5em;
background-color: #f8fafc;
border-left: 4px solid #6366f1;
padding-left: 1rem;
}
/* Specific styling for Plotly graph containers */
#plotly-graph { /* Changed from #plot2d, #plot3d */
border-radius: 0.5rem;
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
padding: 1rem;
background-color: #ffffff;
min-height: 500px;
height: 600px;
width: 100%;
}
</style>
<script src="https://cdn.plot.ly/plotly-2.32.0.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<div class="svm-page-wrapper">
<div class="container">
<h1 class="text-3xl font-bold mb-4 text-center">💡 Interactive Dynamic Linear SVM Visualization</h1>
<p class="mb-6 text-center text-gray-600">
Explore a dynamically trained Linear Support Vector Machine, adjusting parameters and adding new data points.
</p>
<!-- New Controls Section from your provided code -->
<div class="controls grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
<div class="flex flex-col">
<label for="dimensions" class="text-gray-700 text-sm font-semibold mb-1">Dimensions:</label>
<select id="dimensions" class="p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="2D">2D</option>
<option value="3D">3D</option>
</select>
</div>
<div class="flex flex-col">
<label for="data-points-per-class" class="text-gray-700 text-sm font-semibold mb-1">Initial Data Points per Class:</label>
<input type="number" id="data-points-per-class" value="20" min="5" max="100" class="p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="flex flex-col">
<label for="learning-rate" class="text-gray-700 text-sm font-semibold mb-1">Learning Rate:</label>
<input type="number" id="learning-rate" value="0.01" step="0.001" min="0.001" max="0.1" class="p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="flex flex-col">
<label for="iterations" class="text-gray-700 text-sm font-semibold mb-1">Iterations:</label>
<input type="number" id="iterations" value="1000" step="100" min="100" max="5000" class="p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="flex flex-col">
<label for="regularization-c" class="text-gray-700 text-sm font-semibold mb-1">Regularization (C):</label>
<input type="number" id="regularization-c" value="0.1" step="0.01" min="0.01" max="1" class="p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="flex flex-col">
<label for="new-point-x" class="text-gray-700 text-sm font-semibold mb-1">New Point X:</label>
<input type="number" id="new-point-x" value="0" step="0.1" class="p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="flex flex-col" id="new-point-y-wrapper">
<label for="new-point-y" class="text-gray-700 text-sm font-semibold mb-1">New Point Y:</label>
<input type="number" id="new-point-y" value="0" step="0.1" class="p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="flex flex-col hidden" id="new-point-z-wrapper">
<label for="new-point-z" class="text-gray-700 text-sm font-semibold mb-1">New Point Z:</label>
<input type="number" id="new-point-z" value="0" step="0.1" class="p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<button id="add-point-btn" class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
Add New Point & Classify
</button>
<button id="reset-data-btn" class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-md focus:outline-none focus:ring-2 focus:ring-red-500">
Reset Data & Retrain
</button>
</div>
<div id="plotly-graph" class="w-full h-96 md:h-[500px] lg:h-[600px] border border-gray-300 rounded-lg"></div>
<p id="prediction-result" class="mt-4 font-bold text-lg text-center text-gray-800 mb-8"></p>
<a href="/NB+DT+SVM"
class="inline-block bg-blue-600 text-white px-8 py-3 hover:bg-blue-700 transition font-semibold text-lg shadow-md no-underline">
URL SPAM PREDICTOR BY NB+SVM+DT
</a>
<!-- Existing Detailed Explanations Below -->
<div class="mt-10 p-6 bg-green-50 rounded-xl border border-green-200">
<h2 class="text-2xl font-bold mb-6 text-center text-green-700">How SVM Works: Step-by-Step Classification</h2>
<div class="flex flex-wrap justify-center items-center gap-4">
<div class="flow-box bg-green-100">
<span class="text-5xl mb-2">📊</span> <p class="text-lg font-semibold text-green-800">1. Input Labeled Data</p>
<p class="text-sm text-green-600">Points with known classes</p>
</div>
<div class="flow-arrow">&rarr;</div>
<div class="flow-box bg-green-100">
<span class="text-5xl mb-2">📈</span> <p class="text-lg font-semibold text-green-800">2. Map to Higher Dimension</p>
<p class="text-sm text-green-600">(If using Kernel, e.g., RBF)</p>
</div>
<div class="flow-arrow">&rarr;</div>
<div class="flow-box bg-green-100">
<span class="text-5xl mb-2"></span> <p class="text-lg font-semibold text-green-800">3. Find Optimal Hyperplane</p>
<p class="text-sm text-green-600">Maximizing the margin</p>
</div>
<div class="flow-arrow block md:hidden">&darr;</div>
<div class="flow-arrow hidden md:block">&rarr;</div>
<div class="flow-box bg-green-100">
<span class="text-5xl mb-2"></span> <p class="text-lg font-semibold text-green-800">4. Identify Support Vectors</p>
<p class="text-sm text-green-600">Closest points define boundary</p>
</div>
<div class="flow-arrow">&rarr;</div>
<div class="flow-box bg-green-100">
<span class="text-5xl mb-2"></span> <p class="text-lg font-semibold text-green-800">5. Classify New Data</p>
<p class="text-sm text-green-600">Based on hyperplane position</p>
</div>
</div>
<p class="mt-6 text-center text-gray-600 text-sm">
The SVM algorithm processes your input data through several key steps: it first ingests your labeled data, then (optionally, using the kernel trick for non-linear cases) transforms it into a higher-dimensional space. Next, it determines the best possible decision boundary (hyperplane) that maximally separates the classes while minimizing errors. The crucial data points closest to this boundary are identified as 'Support Vectors' as they fundamentally define the separation. Finally, any new, unlabeled data point is classified based on which side of this optimized hyperplane it falls.
</p>
</div>
<div class="mt-8 p-6 bg-gray-50 rounded-lg border border-gray-200">
<h2 class="text-2xl font-bold mb-4 text-center text-blue-700">Understanding Support Vector Machines (SVM)</h2>
<p class="mb-4 text-gray-700">
Support Vector Machines (SVMs) are powerful supervised learning models used for classification and regression. The core idea behind SVM is to find the <span class="highlight-bold">optimal hyperplane</span> that best separates data points of different classes in a high-dimensional space.
</p>
<h3 class="text-xl font-semibold mb-2">Key Concepts of Support Vector Machine</h3>
<ul class="list-disc list-inside text-gray-700 mb-4">
<li class="mb-2">
<span class="highlight-bold">Hyperplane:</span> A decision boundary separating different classes in feature space and is represented by the equation $wx + b = 0$ in linear classification.
</li>
<li class="mb-2">
<span class="highlight-purple">Support Vectors:</span> The closest data points to the hyperplane, crucial for determining the hyperplane and margin in SVM. In our plot, they are highlighted with a <span class="highlight-purple">purple ring</span>.
</li>
<li class="mb-2">
<span class="highlight-bold">Margin:</span> The distance between the hyperplane and the support vectors. SVM aims to maximize this margin for better classification performance.
</li>
<li class="mb-2">
<span class="highlight-bold">Kernel:</span> A function that maps data to a higher-dimensional space enabling SVM to handle non-linearly separable data.
</li>
<li class="mb-2">
<span class="highlight-bold">Hard Margin:</span> A maximum-margin hyperplane that perfectly separates the data without misclassifications.
</li>
<li class="mb-2">
<span class="highlight-bold">Soft Margin:</span> Allows some misclassifications by introducing slack variables, balancing margin maximization and misclassification penalties when data is not perfectly separable.
</li>
<li class="mb-2">
<span class="highlight-bold">C:</span> A regularization parameter that controls the trade-off between margin maximization and misclassification penalties. A higher C value forces stricter penalty for misclassifications.
</li>
<li class="mb-2">
<span class="highlight-bold">Hinge Loss:</span> A loss function penalizing misclassified points or margin violations and is combined with regularization in SVM. If a data point is correctly classified and within the margin there is no penalty (loss = 0). If a point is incorrectly classified or violates the margin the hinge loss increases proportionally to the distance of the violation.
</li>
<li class="mb-2">
<span class="highlight-bold">Dual Problem:</span> Involves solving for Lagrange multipliers associated with support vectors, facilitating the kernel trick and efficient computation.
</li>
</ul>
<h3 class="text-xl font-semibold mb-2">How does Support Vector Machine Algorithm Work?</h3>
<p class="mb-4 text-gray-700">
The key idea behind the SVM algorithm is to find the hyperplane that best separates two classes by maximizing the margin between them. This margin is the distance from the hyperplane to the nearest data points (support vectors) on each side.
</p>
<p class="mb-4 text-gray-700">
The best hyperplane also known as the "hard margin" is the one that maximizes the distance between the hyperplane and the nearest data points from both classes. This ensures a clear separation between the classes. Let's consider a scenario like shown below where we have one blue ball in the boundary of the red ball (referring to a conceptual image similar to the one you provided earlier).
</p>
<h4 class="text-lg font-semibold mb-2">How does SVM classify the data?</h4>
<p class="mb-4 text-gray-700">
The blue ball in the boundary of red ones is an outlier of blue balls. The SVM algorithm has the characteristics to ignore the outlier and finds the best hyperplane that maximizes the margin. SVM is robust to outliers. A soft margin allows for some misclassifications or violations of the margin to improve generalization. The SVM optimizes the following equation to balance margin maximization and penalty minimization:
</p>
<div class="math">
$$ \text{Objective Function} = \left(\frac{1}{\text{margin}}\right) + \lambda \sum \text{penalty} $$
</div>
<p class="mb-4 text-gray-700">
The penalty used for violations is often hinge loss which has the following behavior:
</p>
<ul class="list-disc list-inside text-gray-700 mb-4">
<li>If a data point is correctly classified and within the margin there is no penalty (loss = 0).</li>
<li>If a point is incorrectly classified or violates the margin the hinge loss increases proportionally to the distance of the violation.</li>
</ul>
<p class="mb-4 text-gray-700">
Till now we were talking about linearly separable data that separates groups of blue balls and red balls by a straight line/linear line.
</p>
<h3 class="text-xl font-semibold mb-2">What to do if data are not linearly separable?</h3>
<p class="mb-4 text-gray-700">
When data is not linearly separable i.e it can't be divided by a straight line, SVM uses a technique called <span class="highlight-bold">kernels</span> to map the data into a higher-dimensional space where it becomes separable. This transformation helps SVM find a decision boundary even for non-linear data.
</p>
<p class="mb-4 text-gray-700">
A kernel is a function that maps data points into a higher-dimensional space without explicitly computing the coordinates in that space. This allows SVM to work efficiently with non-linear data by implicitly performing the mapping. For example consider data points that are not linearly separable. By applying a kernel function SVM transforms the data points into a higher-dimensional space where they become linearly separable.
</p>
<ul class="list-disc list-inside text-gray-700 mb-4">
<li><span class="highlight-bold">Linear Kernel:</span> For linear separability.</li>
<li><span class="highlight-bold">Polynomial Kernel:</span> Maps data into a polynomial space.</li>
<li><span class="highlight-bold">Radial Basis Function (RBF) Kernel:</span> Transforms data into a space based on distances between data points.</li>
</ul>
<p class="mb-4 text-gray-700">
In this case (referring to your 1D to 2D mapping image) the new variable `y` is created as a function of distance from the origin.
</p>
<h3 class="text-xl font-semibold mb-2">Mathematical Computation of SVM</h3>
<p class="mb-4 text-gray-700">
Consider a binary classification problem with two classes, labeled as $+1$ and $-1$. We have a training dataset consisting of input feature vectors $X$ and their corresponding class labels $Y$. The equation for the linear hyperplane can be written as:
</p>
<div class="math">
$$ w^T x + b = 0 $$
</div>
<p class="mb-4 text-gray-700">
Where:
</p>
<ul class="list-disc list-inside text-gray-700 mb-4">
<li>$w$ is the normal vector to the hyperplane (the direction perpendicular to it).</li>
<li>$b$ is the offset or bias term representing the distance of the hyperplane from the origin along the normal vector $w$.</li>
</ul>
<h4 class="text-lg font-semibold mb-2">Distance from a Data Point to the Hyperplane</h4>
<p class="mb-4 text-gray-700">
The distance between a data point $x_i$ and the decision boundary can be calculated as:
</p>
<div class="math">
$$ d_i = \frac{w^T x_i + b}{\|w\|} $$
</div>
<p class="mb-4 text-gray-700">
where $\|w\|$ represents the Euclidean norm of the weight vector $w$.
</p>
<h4 class="text-lg font-semibold mb-2">Linear SVM Classifier</h4>
<p class="mb-4 text-gray-700">
The predicted label $\hat{y}$ of a data point is given by:
</p>
<div class="math">
$$ \hat{y} = \begin{cases} 1 & : w^T x + b \geq 0 \\ 0 & : w^T x + b < 0 \end{cases} $$
</div>
<h4 class="text-lg font-semibold mb-2">Optimization Problem for SVM (Hard Margin)</h4>
<p class="mb-4 text-gray-700">
For a linearly separable dataset the goal is to find the hyperplane that maximizes the margin between the two classes while ensuring that all data points are correctly classified. This leads to the following optimization problem:
</p>
<div class="math">
$$ \underset{w,b}{\text{minimize}} \quad \frac{1}{2} \|w\|^2 $$
</div>
<p class="mb-4 text-gray-700">
Subject to the constraint:
</p>
<div class="math">
$$ y_i (w^T x_i + b) \geq 1 \quad \text{for } i = 1, 2, 3, \ldots, m $$
</div>
<p class="mb-4 text-gray-700">
Where:
</p>
<ul class="list-disc list-inside text-gray-700 mb-4">
<li>$y_i$ is the class label (+1 or -1) for each training instance.</li>
<li>$x_i$ is the feature vector for the $i$-th training instance.</li>
<li>$m$ is the total number of training instances.</li>
</ul>
<p class="mb-4 text-gray-700">
The condition $y_i (w^T x_i + b) \geq 1$ ensures that each data point is correctly classified and lies outside the margin.
</p>
<h4 class="text-lg font-semibold mb-2">Soft Margin in Linear SVM Classifier</h4>
<p class="mb-4 text-gray-700">
In the presence of outliers or non-separable data the SVM allows some misclassification by introducing slack variables $\zeta_i$. The optimization problem is modified as:
</p>
<div class="math">
$$ \underset{w,b}{\text{minimize}} \quad \frac{1}{2} \|w\|^2 + C \sum_{i=1}^{m} \zeta_i $$
</div>
<p class="mb-4 text-gray-700">
Subject to the constraints:
</p>
<div class="math">
$$ y_i (w^T x_i + b) \geq 1 - \zeta_i \quad \text{and} \quad \zeta_i \geq 0 \quad \text{for } i = 1, 2, \ldots, m $$
</div>
<p class="mb-4 text-gray-700">
Where:
</p>
<ul class="list-disc list-inside text-gray-700 mb-4">
<li>$C$ is a regularization parameter that controls the trade-off between margin maximization and penalty for misclassifications.</li>
<li>$\zeta_i$ are slack variables that represent the degree of violation of the margin by each data point.</li>
</ul>
<h4 class="text-lg font-semibold mb-2">Dual Problem for SVM</h4>
<p class="mb-4 text-gray-700">
The dual problem involves maximizing the Lagrange multipliers associated with the support vectors. This transformation allows solving the SVM optimization using kernel functions for non-linear classification.
</p>
<p class="mb-4 text-gray-700">
The dual objective function is given by:
</p>
<div class="math">
$$ \underset{\alpha}{\text{maximize}} \quad \frac{1}{2} \sum_{i=1}^{m} \sum_{j=1}^{m} \alpha_i \alpha_j t_i t_j K(x_i, x_j) - \sum_{i=1}^{m} \alpha_i $$
</div>
<p class="mb-4 text-gray-700">
Where:
</p>
<ul class="list-disc list-inside text-gray-700 mb-4">
<li>$\alpha_i$ are the Lagrange multipliers associated with the $i$-th training sample.</li>
<li>$t_i$ is the class label for the $i$-th training sample.</li>
<li>$K(x_i, x_j)$ is the kernel function that computes the similarity between data points $x_i$ and $x_j$. The kernel allows SVM to handle non-linear classification problems by mapping data into a higher-dimensional space.</li>
</ul>
<p class="mb-4 text-gray-700">
The dual formulation optimizes the Lagrange multipliers $\alpha_i$ and the support vectors are those training samples where $\alpha_i > 0$.
</p>
<h4 class="text-lg font-semibold mb-2">SVM Decision Boundary</h4>
<p class="mb-4 text-gray-700">
Once the dual problem is solved, the decision boundary is given by:
</p>
<div class="math">
$$ w = \sum_{i=1}^{m} \alpha_i t_i K(x_i, x) + b $$
</div>
<p class="mb-4 text-gray-700">
Where $w$ is the weight vector, $x$ is the test data point and $b$ is the bias term. Finally the bias term $b$ is determined by the support vectors, which satisfy:
</p>
<div class="math">
$$ t_i (w^T x_i - b) = 1 \implies b = w^T x_i - t_i $$
</div>
<p class="mb-4 text-gray-700">
Where $x_i$ is any support vector.
</p>
<p class="mb-4 text-gray-700">
This completes the mathematical framework of the Support Vector Machine algorithm which allows for both linear and non-linear classification using the dual problem and kernel trick.
</p>
<h3 class="text-xl font-semibold mb-2">Types of Support Vector Machine</h3>
<ul class="list-disc list-inside text-gray-700 mb-4">
<li><span class="highlight-bold">Linear SVM:</span> Linear SVMs use a linear decision boundary to separate the data points of different classes. When the data can be precisely linearly separated, linear SVMs are very suitable. This means that a single straight line (in 2D) or a hyperplane (in higher dimensions) can entirely divide the data points into their respective classes. A hyperplane that maximizes the margin between the classes is the decision boundary.</li>
<li><span class="highlight-bold">Non-Linear SVM:</span> Non-Linear SVM can be used to classify data when it cannot be separated into two classes by a straight line (in the case of 2D). By using kernel functions, nonlinear SVMs can handle nonlinearly separable data. The original input data is transformed by these kernel functions into a higher-dimensional feature space where the data points can be linearly separated. A linear SVM is used to locate a nonlinear decision boundary in this modified space.</li>
</ul>
</div>
</div>
</div>
<script>
// --- LinearSVM Class (from your svm.js) ---
class LinearSVM {
constructor(learningRate = 0.01, iterations = 1000, C = 0.1) {
this.weights = []; // w
this.bias = 0; // b
this.learningRate = learningRate;
this.iterations = iterations;
this.C = C; // Regularization parameter for soft margin
this.dimensions = 0;
this.classes = [-1, 1]; // Assume binary classification for simplicity: -1 and 1
}
/**
* Initializes weights and bias based on feature dimensions.
* @param {number} numFeatures
*/
_initializeParameters(numFeatures) {
this.dimensions = numFeatures;
this.weights = Array(numFeatures).fill(0).map(() => Math.random() * 0.1); // Small random initialization
this.bias = 0;
}
/**
* Trains the Linear SVM model using a simplified SGD-like approach.
* Assumes class labels are -1 and 1.
* @param {Array<Array<number>>} X - Array of feature vectors.
* @param {Array<number>} y - Array of class labels (-1 or 1).
*/
fit(X, y) {
if (X.length === 0 || y.length === 0) {
console.warn("No data to train the SVM model.");
return;
}
const numFeatures = X[0].length;
this._initializeParameters(numFeatures);
// Map original class labels (e.g., 0, 1) to -1, 1
const mappedY = y.map(label => label === 0 ? -1 : 1);
for (let iter = 0; iter < this.iterations; iter++) {
for (let i = 0; i < X.length; i++) {
const xi = X[i];
const yi = mappedY[i];
// Prediction: dot product (w . x) + b
const prediction = xi.reduce((sum, val, idx) => sum + val * this.weights[idx], 0) + this.bias;
// Check for misclassification or within margin for soft margin
if (yi * prediction < 1) {
// Update weights and bias (SGD update rule for hinge loss)
for (let j = 0; j < numFeatures; j++) {
this.weights[j] = this.weights[j] - this.learningRate * (this.weights[j] - this.C * yi * xi[j]);
}
this.bias = this.bias - this.learningRate * (-this.C * yi);
} else {
// Regularization term (gradient of ||w||^2)
for (let j = 0; j < numFeatures; j++) {
this.weights[j] = this.weights[j] - this.learningRate * this.weights[j];
}
}
}
}
console.log("SVM Model trained:", this.weights, this.bias);
}
/**
* Predicts the class for a given data point.
* @param {Array<number>} observation - The feature vector to classify.
* @returns {number} The predicted class label (0 or 1).
*/
predict(observation) {
if (this.weights.length === 0) {
console.warn("SVM model not trained yet.");
return null;
}
const prediction = observation.reduce((sum, val, idx) => sum + val * this.weights[idx], 0) + this.bias;
return prediction >= 0 ? 1 : 0; // Map back to 0 or 1
}
/**
* Generates data for the decision boundary (2D).
* The decision boundary is w1*x + w2*y + b = 0
* So, y = (-w1*x - b) / w2
* @param {number} xMin
* @param {number} xMax
* @returns {Array<Object>} Traces for decision boundary and margins.
*/
generateDecisionBoundary2D(xMin, xMax) {
if (this.weights.length < 2 || Math.abs(this.weights[1]) < 1e-6) { // Avoid division by very small numbers or zero
// Handle vertical line case or untrained model
if (this.weights.length >= 1 && Math.abs(this.weights[0]) >= 1e-6) {
// Vertical line: x = -b / w0
const xIntercept = -this.bias / this.weights[0];
return [{
x: [xIntercept, xIntercept],
y: [-100, 100], // Extend far enough
mode: 'lines',
type: 'scatter',
name: 'Decision Boundary',
line: { color: 'black', width: 2 },
hoverinfo: 'name'
}];
}
return []; // No boundary if not trained or invalid weights
}
const x_values = [xMin, xMax];
const y_boundary = x_values.map(x => (-this.weights[0] * x - this.bias) / this.weights[1]);
// Calculate margin lines: w.x + b = 1 and w.x + b = -1
const y_margin_plus = x_values.map(x => (1 - this.weights[0] * x - this.bias) / this.weights[1]);
const y_margin_minus = x_values.map(x => (-1 - this.weights[0] * x - this.bias) / this.weights[1]);
return [
{
x: x_values,
y: y_boundary,
mode: 'lines',
type: 'scatter',
name: 'Decision Boundary',
line: { color: 'black', width: 2 },
hoverinfo: 'name'
},
{
x: x_values,
y: y_margin_plus,
mode: 'lines',
type: 'scatter',
name: 'Margin (+1)',
line: { color: 'gray', width: 1, dash: 'dash' },
hoverinfo: 'name'
},
{
x: x_values,
y: y_margin_minus,
mode: 'lines',
type: 'scatter',
name: 'Margin (-1)',
line: { color: 'gray', width: 1, dash: 'dash' },
hoverinfo: 'name'
}
];
}
/**
* Generates data for the decision surface (3D).
* The decision surface is w1*x + w2*y + w3*z + b = 0
* So, z = (-w1*x - w2*y - b) / w3
* @param {number} xMin
* @param {number} xMax
* @param {number} yMin
* @param {number} yMax
* @param {number} resolution
* @returns {Array<Object>} Traces for decision plane and margin planes.
*/
generateDecisionBoundary3D(xMin, xMax, yMin, yMax, resolution = 20) {
if (this.weights.length < 3 || Math.abs(this.weights[2]) < 1e-6) { // Avoid division by very small numbers or zero
console.warn("SVM cannot generate 3D boundary with given weights (w3 is zero or not enough dimensions).");
return [];
}
const x_values = Array.from({ length: resolution }, (_, i) => xMin + (xMax - xMin) * i / (resolution - 1));
const y_values = Array.from({ length: resolution }, (_, i) => yMin + (yMax - yMin) * i / (resolution - 1));
const z_boundary = [];
const z_margin_plus = [];
const z_margin_minus = [];
for (let i = 0; i < resolution; i++) {
const row_boundary = [];
const row_margin_plus = [];
const row_margin_minus = [];
for (let j = 0; j < resolution; j++) {
const x = x_values[j];
const y = y_values[i];
// Decision boundary: w1*x + w2*y + w3*z + b = 0 => z = (-w1*x - w2*y - b) / w3
row_boundary.push((-this.weights[0] * x - this.weights[1] * y - this.bias) / this.weights[2]);
// Margin +1: w1*x + w2*y + w3*z + b = 1 => z = (1 - w1*x - w2*y - b) / w3
row_margin_plus.push((1 - this.weights[0] * x - this.weights[1] * y - this.bias) / this.weights[2]);
// Margin -1: w1*x + w2*y + w3*z + b = -1 => z = (-1 - w1*x - w2*y - b) / w3
row_margin_minus.push((-1 - this.weights[0] * x - this.weights[1] * y - this.bias) / this.weights[2]);
}
z_boundary.push(row_boundary);
z_margin_plus.push(row_margin_plus);
z_margin_minus.push(row_margin_minus);
}
return [
{
z: z_boundary,
x: x_values,
y: y_values,
type: 'surface',
name: 'Decision Plane',
colorscale: [[0, 'rgb(0,0,0)'], [1, 'rgb(0,0,0)']], // Black for the plane
opacity: 0.7,
showscale: false,
hoverinfo: 'name'
},
{
z: z_margin_plus,
x: x_values,
y: y_values,
type: 'surface',
name: 'Margin (+1)',
colorscale: [[0, 'rgb(128,128,128)'], [1, 'rgb(128,128,128)']], // Gray for margin
opacity: 0.3,
showscale: false,
hoverinfo: 'name'
},
{
z: z_margin_minus,
x: x_values,
y: y_values,
type: 'surface',
name: 'Margin (-1)',
colorscale: [[0, 'rgb(128,128,128)'], [1, 'rgb(128,128,128)']], // Gray for margin
opacity: 0.3,
showscale: false,
hoverinfo: 'name'
}
];
}
}
// --- Main Logic (from your main.js, adapted) ---
document.addEventListener('DOMContentLoaded', () => {
const plotlyGraph = document.getElementById('plotly-graph');
const dimensionsSelect = document.getElementById('dimensions');
const dataPointsPerClassInput = document.getElementById('data-points-per-class');
const learningRateInput = document.getElementById('learning-rate');
const iterationsInput = document.getElementById('iterations');
const regularizationCInput = document.getElementById('regularization-c');
const addPointBtn = document.getElementById('add-point-btn');
const resetDataBtn = document.getElementById('reset-data-btn');
const newPointXInput = document.getElementById('new-point-x');
const newPointYInput = document.getElementById('new-point-y');
const newPointZInput = document.getElementById('new-point-z');
const newPointYWrapper = document.getElementById('new-point-y-wrapper');
const newPointZWrapper = document.getElementById('new-point-z-wrapper');
const predictionResultDisplay = document.getElementById('prediction-result'); // New element for prediction output
let currentData = []; // Stores { x, y, (z), class }
let linearSVMModel; // Will be initialized based on current parameters
let currentDimensions = dimensionsSelect.value; // "2D" or "3D"
const numClasses = 2; // Fixed for this binary SVM implementation
const classColors = [
'#1f77b4', '#ff7f0e' // Blue for class 0, Orange for class 1
];
// --- Helper Functions ---
function generateRandomData(numPointsPerClass, dimensions) {
const data = [];
const classCenters = [
{ x: -2, y: -2, z: -2 }, // Center for class 0
{ x: 2, y: 2, z: 2 } // Center for class 1
];
for (let c = 0; c < numClasses; c++) {
const centerX = classCenters[c].x;
const centerY = classCenters[c].y;
const centerZ = classCenters[c].z;
for (let i = 0; i < numPointsPerClass; i++) {
const x = centerX + (Math.random() - 0.5) * 3; // Spread of 3 units
const y = centerY + (Math.random() - 0.5) * 3;
if (dimensions === "2D") {
data.push({ x: x, y: y, class: c });
} else { // 3D
const z = centerZ + (Math.random() - 0.5) * 3;
data.push({ x: x, y: y, z: z, class: c });
}
}
}
return data;
}
function prepareDataForModel(data) {
const X = data.map(d => {
if (currentDimensions === "2D") {
return [d.x, d.y];
} else {
return [d.x, d.y, d.z];
}
});
const y = data.map(d => d.class);
return { X, y };
}
function createPlotlyTraces(data, type) {
const traces = [];
// Classes are fixed to 0 and 1 for this SVM
[0, 1].forEach(cls => {
const classData = data.filter(d => d.class === cls);
if (type === "2D") {
traces.push({
x: classData.map(d => d.x),
y: classData.map(d => d.y),
mode: 'markers',
type: 'scatter',
name: `Class ${cls}`,
marker: {
color: classColors[cls],
size: 8,
line: {
color: 'white',
width: 1
}
}
});
} else { // 3D
traces.push({
x: classData.map(d => d.x),
y: classData.map(d => d.y),
z: classData.map(d => d.z),
mode: 'markers',
type: 'scatter3d',
name: `Class ${cls}`,
marker: {
color: classColors[cls],
size: 6,
line: {
color: 'white',
width: 1
}
}
});
}
});
return traces;
}
function getAxisRanges(data, dimensions) {
if (data.length === 0) {
return {
x: [-5, 5],
y: [-5, 5],
z: [-5, 5]
};
}
const minX = Math.min(...data.map(d => d.x)) - 2;
const maxX = Math.max(...data.map(d => d.x)) + 2;
const minY = Math.min(...data.map(d => d.y)) - 2;
const maxY = Math.max(...data.map(d => d.y)) + 2;
if (dimensions === "2D") {
return {
x: [minX, maxX],
y: [minY, maxY]
};
} else { // 3D
const minZ = Math.min(...data.map(d => d.z)) - 2;
const maxZ = Math.max(...data.map(d => d.z)) + 2;
return {
x: [minX, maxX],
y: [minY, maxY],
z: [minZ, maxZ]
};
}
}
function updateGraph() {
predictionResultDisplay.innerText = ''; // Clear previous prediction result
const { X, y } = prepareDataForModel(currentData);
if (X.length === 0) {
Plotly.purge(plotlyGraph); // Clear graph if no data
return;
}
// Initialize/re-initialize SVM model with current parameters
linearSVMModel = new LinearSVM(
parseFloat(learningRateInput.value),
parseInt(iterationsInput.value),
parseFloat(regularizationCInput.value)
);
linearSVMModel.fit(X, y);
const plotlyTraces = createPlotlyTraces(currentData, currentDimensions);
let layout;
const ranges = getAxisRanges(currentData, currentDimensions);
if (currentDimensions === "2D") {
const boundaryTraces = linearSVMModel.generateDecisionBoundary2D(
ranges.x[0], ranges.x[1]
);
plotlyTraces.unshift(...boundaryTraces); // Add boundary as background
layout = {
title: 'Linear SVM Classification (2D)',
xaxis: { title: 'Feature 1', range: ranges.x },
yaxis: { title: 'Feature 2', range: ranges.y },
hovermode: 'closest',
showlegend: true
};
Plotly.newPlot(plotlyGraph, plotlyTraces, layout);
} else { // 3D
const boundaryTraces = linearSVMModel.generateDecisionBoundary3D(
ranges.x[0], ranges.x[1],
ranges.y[0], ranges.y[1]
);
plotlyTraces.unshift(...boundaryTraces); // Add boundary as background
layout = {
title: 'Linear SVM Classification (3D)',
scene: {
xaxis: { title: 'Feature 1', range: ranges.x },
yaxis: { title: 'Feature 2', range: ranges.y },
zaxis: { title: 'Feature 3', range: ranges.z },
aspectmode: 'cube'
},
hovermode: 'closest',
showlegend: true
};
Plotly.newPlot(plotlyGraph, plotlyTraces, layout);
}
}
// --- Event Listeners ---
dimensionsSelect.addEventListener('change', (event) => {
currentDimensions = event.target.value;
if (currentDimensions === "2D") {
newPointZWrapper.classList.add('hidden');
} else {
newPointZWrapper.classList.remove('hidden');
}
// Regenerate initial data for new dimensions
currentData = generateRandomData(
parseInt(dataPointsPerClassInput.value),
currentDimensions
);
updateGraph();
});
dataPointsPerClassInput.addEventListener('change', () => {
currentData = generateRandomData(
parseInt(dataPointsPerClassInput.value),
currentDimensions
);
updateGraph();
});
learningRateInput.addEventListener('change', updateGraph);
iterationsInput.addEventListener('change', updateGraph);
regularizationCInput.addEventListener('change', updateGraph);
addPointBtn.addEventListener('click', () => {
const x = parseFloat(newPointXInput.value);
const y = parseFloat(newPointYInput.value);
let point = [x, y];
if (currentDimensions === "3D") {
const z = parseFloat(newPointZInput.value);
point.push(z);
}
const predictedClass = linearSVMModel.predict(point);
if (predictedClass !== null) {
const newPointData = { x: x, y: y, class: predictedClass };
if (currentDimensions === "3D") {
newPointData.z = point[2];
}
currentData.push(newPointData);
updateGraph();
predictionResultDisplay.innerText = `New point (${point.join(', ')}) classified as: Class ${predictedClass}`;
} else {
predictionResultDisplay.innerText = "SVM model not trained yet or an error occurred. Try resetting data.";
}
});
resetDataBtn.addEventListener('click', () => {
currentData = generateRandomData(
parseInt(dataPointsPerClassInput.value),
currentDimensions
);
updateGraph();
});
// Initial setup
currentData = generateRandomData(
parseInt(dataPointsPerClassInput.value),
currentDimensions
);
updateGraph();
MathJax.typeset(); // Ensure MathJax equations are rendered on initial load
});
</script>
{% endblock %}