import numpy as np import csv class SVM: def __init__(self, C=1.0, lr=0.001, epochs=1000, kernel='linear', degree=3, gamma=None, coef0=1, random_features=100): """ SVM classifier with linear, polynomial, or (approximate) RBF kernel. Trains via stochastic subgradient descent on hinge loss. """ self.C = C # Regularization parameter self.lr = lr # Learning rate self.epochs = epochs # Number of epochs for training self.kernel = kernel # 'linear', 'poly', or 'rbf' self.degree = degree # Degree for polynomial kernel self.coef0 = coef0 # Offset for polynomial kernel (not used in feature expansion here) self.gamma = gamma # Kernel scale for RBF or polynomial (default 1/n_features) self.random_features = random_features # Number of random Fourier features for RBF self.w = None # Weight vector (in feature space) self.b = 0.0 # Bias term # For RBF approximation: random projection parameters self.W = None self.b_rff = None def _poly_features(self, X): """ Explicit polynomial feature expansion up to self.degree. E.g., degree=2 for features [x1, x2] gives [x1, x2, x1^2, x1*x2, x2^2]. """ from itertools import combinations_with_replacement n_samples, n_features = X.shape features = [] for deg in range(1, self.degree+1): for combo in combinations_with_replacement(range(n_features), deg): feat = np.ones(n_samples) for i in combo: feat *= X[:, i] features.append(feat) if len(features) > 0: return np.vstack(features).T else: return np.zeros((n_samples, 0)) def _rbf_features(self, X): """ Random Fourier feature mapping for RBF kernel approximation: phi(x) = sqrt(2/D) * [cos(w_i^T x + b_i)]_i. """ if self.gamma is None: self.gamma = 1.0 / X.shape[1] D = self.random_features if self.W is None: self.W = np.sqrt(2 * self.gamma) * np.random.randn(D, X.shape[1]) if self.b_rff is None: self.b_rff = 2 * np.pi * np.random.rand(D) # Compute RFF features return np.sqrt(2.0 / D) * np.cos(np.dot(X, self.W.T) + self.b_rff) def fit(self, X, y): """ Train the SVM. y should be encoded as {-1, +1}. Uses subgradient descent on the hinge loss (primal form). """ X = np.array(X, dtype=float) y = np.array(y, dtype=float) # Feature mapping based on kernel choice if self.kernel == 'linear': X_train = X.copy() elif self.kernel == 'poly': X_train = self._poly_features(X) elif self.kernel == 'rbf': X_train = self._rbf_features(X) else: raise ValueError("Unsupported kernel type") n_samples, n_features = X_train.shape # Initialize weights and bias self.w = np.zeros(n_features) self.b = 0.0 # Ensure labels are -1 or +1 y_signed = np.where(y <= 0, -1, 1) # Stochastic subgradient descent for epoch in range(self.epochs): for i in range(n_samples): xi, yi = X_train[i], y_signed[i] margin = yi * (np.dot(self.w, xi) + self.b) if margin < 1: # Hinge loss is active: update gradient includes -C*y*x grad_w = self.w - self.C * yi * xi grad_b = -self.C * yi else: # No loss: only regularizer gradient grad_w = self.w grad_b = 0.0 # Update parameters self.w -= self.lr * grad_w self.b -= self.lr * grad_b def decision_function(self, X): """ Compute the raw decision score (w·x + b) for samples X. """ X = np.array(X, dtype=float) if self.kernel == 'linear': X_eval = X elif self.kernel == 'poly': X_eval = self._poly_features(X) elif self.kernel == 'rbf': X_eval = self._rbf_features(X) else: raise ValueError("Unsupported kernel type") return np.dot(X_eval, self.w) + self.b def predict(self, X): """ Predict class labels {-1, +1} for samples in X. """ scores = self.decision_function(X) return np.where(scores >= 0, 1, -1) def predict_proba(self, X): """ (Approximate) probability estimates for binary classification. Uses a logistic function on the decision scores. Returns shape (n_samples, 2) with columns [P(y=-1), P(y=+1)]. """ scores = self.decision_function(X) # Sigmoid for positive class p_pos = 1.0 / (1.0 + np.exp(-scores)) p_neg = 1.0 - p_pos return np.vstack([p_neg, p_pos]).T class MultiClassSVM: def __init__(self, C=1.0, lr=0.001, epochs=1000, kernel='linear', degree=3, gamma=None, coef0=1): """ One-vs-Rest multiclass SVM. Trains one binary SVM per class. """ self.C = C self.lr = lr self.epochs = epochs self.kernel = kernel self.degree = degree self.gamma = gamma self.coef0 = coef0 self.classes_ = None self.models = {} def fit(self, X, y): """ Fit one SVM for each class. """ X = np.array(X, dtype=float) y = np.array(y) self.classes_ = np.unique(y) for cls in self.classes_: # Prepare binary labels: +1 for current class, -1 for all others y_binary = np.where(y == cls, 1, -1) svm = SVM(C=self.C, lr=self.lr, epochs=self.epochs, kernel=self.kernel, degree=self.degree, gamma=self.gamma, coef0=self.coef0) svm.fit(X, y_binary) self.models[cls] = svm def predict(self, X): """ Predict multiclass labels by picking the class with highest decision score. """ X = np.array(X, dtype=float) # Compute decision score for each class model scores = np.vstack([self.models[cls].decision_function(X) for cls in self.classes_]).T # Choose class index with max score idx = np.argmax(scores, axis=1) return self.classes_[idx] def predict_proba(self, X): """ Return (approximate) class probabilities via softmax on the decision scores. """ X = np.array(X, dtype=float) scores = np.vstack([self.models[cls].decision_function(X) for cls in self.classes_]).T # Softmax for probabilities exp_scores = np.exp(scores - np.max(scores, axis=1, keepdims=True)) probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True) return probs class SVR: def __init__(self, C=1.0, lr=0.001, epochs=1000, epsilon=0.1, kernel='linear', degree=3, gamma=None, coef0=1, random_features=100): self.C = C self.lr = lr self.epochs = epochs self.epsilon = epsilon self.kernel = kernel self.degree = degree self.gamma = gamma self.coef0 = coef0 self.random_features = random_features self.w = None self.b = 0.0 self.W = None self.b_rff = None def _poly_features(self, X): from itertools import combinations_with_replacement n_samples, n_features = X.shape features = [] for deg in range(1, self.degree+1): for combo in combinations_with_replacement(range(n_features), deg): feat = np.ones(n_samples) for i in combo: feat *= X[:, i] features.append(feat) if len(features) > 0: return np.vstack(features).T else: return np.zeros((n_samples, 0)) def _rbf_features(self, X): if self.gamma is None: self.gamma = 1.0 / X.shape[1] D = self.random_features if self.W is None: self.W = np.sqrt(2 * self.gamma) * np.random.randn(D, X.shape[1]) if self.b_rff is None: self.b_rff = 2 * np.pi * np.random.rand(D) return np.sqrt(2.0 / D) * np.cos(np.dot(X, self.W.T) + self.b_rff) def fit(self, X, y): """ Train the SVR model. Uses subgradient descent on the ε-insensitive loss. """ X = np.array(X, dtype=float) y = np.array(y, dtype=float) # Feature mapping for kernel if self.kernel == 'linear': X_train = X.copy() elif self.kernel == 'poly': X_train = self._poly_features(X) elif self.kernel == 'rbf': X_train = self._rbf_features(X) else: raise ValueError("Unsupported kernel type") n_samples, n_features = X_train.shape self.w = np.zeros(n_features) self.b = 0.0 # Training loop for epoch in range(self.epochs): for i in range(n_samples): xi, yi = X_train[i], y[i] f = np.dot(self.w, xi) + self.b if yi - f > self.epsilon: # Prediction too low: push up grad_w = self.w - self.C * xi grad_b = -self.C elif f - yi > self.epsilon: # Prediction too high: push down grad_w = self.w + self.C * xi grad_b = self.C else: # Within ε: only regularization gradient grad_w = self.w grad_b = 0.0 self.w -= self.lr * grad_w self.b -= self.lr * grad_b def predict(self, X): """ Predict continuous values for regression. """ X = np.array(X, dtype=float) if self.kernel == 'linear': X_eval = X elif self.kernel == 'poly': X_eval = self._poly_features(X) elif self.kernel == 'rbf': X_eval = self._rbf_features(X) else: raise ValueError("Unsupported kernel type") return np.dot(X_eval, self.w) + self.b # Evaluation metrics and utilities def accuracy_score(y_true, y_pred): """Classification accuracy.""" y_true = np.array(y_true) y_pred = np.array(y_pred) return np.mean(y_true == y_pred) def mean_squared_error(y_true, y_pred): """Mean squared error for regression.""" y_true = np.array(y_true) y_pred = np.array(y_pred) return np.mean((y_true - y_pred)**2) def save_predictions_to_csv(y_true, y_pred, filename): """Save true vs. predicted values to a CSV file.""" with open(filename, mode='w', newline='') as f: writer = csv.writer(f) writer.writerow(['True', 'Predicted']) for true, pred in zip(y_true, y_pred): writer.writerow([true, pred]) # Synthetic data generation def generate_classification_data(binary=True, n_samples=100, n_features=2, n_classes=2, random_state=None): """ Generate synthetic classification data: - Binary: two Gaussian clusters. - Multiclass: 'n_classes' clusters with increasing means. """ rng = np.random.RandomState(random_state) if binary and n_classes == 2: mean1 = rng.randn(n_features) - 1 mean2 = rng.randn(n_features) + 1 cov = np.eye(n_features) * 0.2 X1 = rng.multivariate_normal(mean1, cov, size=n_samples//2) X2 = rng.multivariate_normal(mean2, cov, size=n_samples//2) X = np.vstack([X1, X2]) y = np.hstack([np.zeros(n_samples//2), np.ones(n_samples//2)]) else: X = [] y = [] for cls in range(n_classes): center = rng.randn(n_features) + cls * 2.0 cov = np.eye(n_features) * 0.3 Xc = rng.multivariate_normal(center, cov, size=n_samples//n_classes) yc = np.full(n_samples//n_classes, cls) X.append(Xc); y.append(yc) X = np.vstack(X) y = np.hstack(y) return X, y def generate_regression_data(n_samples=100, n_features=1, noise=0.1, random_state=None): """ Generate synthetic regression data: y = sin(x) + noise. """ rng = np.random.RandomState(random_state) X = np.linspace(-5, 5, n_samples).reshape(-1, n_features) y = np.sin(X).ravel() + noise * rng.randn(n_samples) return X, y # --- Binary Classification Demo --- # Generate binary data X_bin, y_bin = generate_classification_data(binary=True, n_samples=200, n_features=2, n_classes=2, random_state=1) y_bin_signed = np.where(y_bin == 0, -1, 1) # Encode labels as -1/+1 svm = SVM(C=1.0, lr=0.01, epochs=500, kernel='linear') svm.fit(X_bin, y_bin_signed) pred_bin = svm.predict(X_bin) acc_bin = accuracy_score(y_bin_signed, pred_bin) print("Binary SVM accuracy:", acc_bin) # (Optional) Save predictions save_predictions_to_csv(y_bin_signed, pred_bin, "binary_svm_results.csv") # --- Multiclass Classification Demo --- # Generate 3-class data X_multi, y_multi = generate_classification_data(binary=False, n_samples=300, n_features=2, n_classes=3, random_state=42) mc_svm = MultiClassSVM(C=1.0, lr=0.01, epochs=500, kernel='linear') mc_svm.fit(X_multi, y_multi) pred_multi = mc_svm.predict(X_multi) acc_multi = accuracy_score(y_multi, pred_multi) print("Multiclass SVM accuracy:", acc_multi) # Show probability estimates for first few samples (softmaxed scores) probs_multi = mc_svm.predict_proba(X_multi[:5]) print("Class probabilities (first 5):\n", probs_multi) # --- Regression Demo --- # Generate regression data X_reg, y_reg = generate_regression_data(n_samples=100, noise=0.2, random_state=0) # Linear SVR svr = SVR(C=1.0, lr=0.01, epochs=1000, epsilon=0.1, kernel='linear') svr.fit(X_reg, y_reg) pred_reg = svr.predict(X_reg) mse = mean_squared_error(y_reg, pred_reg) print("SVR Mean Squared Error:", mse)