quanttoolbox.svm¶
Python alternatives
Switch to sklearn.svm.SVC/SVR for standalone classification/regression — backed by libsvm/liblinear (compiled C), extremely well optimized and battle-tested. Already verified to match this module to 3+ decimal places. Keep this module only if the SVM needs to be composed with other constraints inside the same solve_qp/cvxpy optimization — that composability is the one thing sklearn's opaque solver can't offer.
svm.svm¶
quanttoolbox.svm.svm
¶
Support Vector Machines: classification and regression, primal and dual formulations.
Ported from QuantToolBox/svm/{svm_classification_dual, svm_classification_primal,svm_regression_dual,svm_regression_primal}.m (QuantToolBox/theo/svm_*.m are byte-identical duplicates, not ported separately).
Consolidation notes:
Every one of the original four functions builds a hand-constructed QP
(block matrices for slack variables, box bounds, one equality constraint)
and calls MATLAB's quadprog directly. Since
quanttoolbox.optim.quadprog.solve_qp already accepts arbitrary
Q/R/equality/inequality/box arguments, each branch below is a direct,
literal translation of the original's Q/R/constraint construction into a
single solve_qp call -- no additional QP machinery is needed. Sign
convention: MATLAB's quadprog(Q, f, ...) minimizes
0.5*x'Qx + f'x; solve_qp(Q, R, ...) minimizes 0.5*x'Qx - R'x,
so every R passed to solve_qp below is the negation of the
original's f.
MATLAB's global SVM_macheps is replaced by
quanttoolbox.config.SVMConfig.
svm_classification_dual(y, x, c=None, loss='hinge', random_start=False, config=None)
¶
SVM classification, dual (kernel-ready) formulation.
c=None (default): hard margin. loss="hinge" (default): standard soft-margin hinge loss (box-constrained dual, alpha in [0, c]). loss="squared_hinge": squared hinge loss (ridge-regularized dual, alpha in [0, inf)).
Original: svm/svm_classification_dual.m
Source code in src/quanttoolbox/svm/svm.py
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | |
svm_classification_primal(y, x, c=None, loss='hinge')
¶
SVM classification, primal formulation.
Same c/loss semantics as svm_classification_dual.
Original: svm/svm_classification_primal.m
Source code in src/quanttoolbox/svm/svm.py
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | |
svm_regression_dual(y, x, c, epsilon=None, config=None)
¶
SVM regression, dual (kernel-ready) formulation.
epsilon=None (default, or a negative value): Least-Squares SVM (ridge-regularized). epsilon=0: epsilon-SVM with epsilon-tube width 0 (box-constrained dual). epsilon>0: standard epsilon-insensitive SVM regression.
Original: svm/svm_regression_dual.m
Source code in src/quanttoolbox/svm/svm.py
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 | |
svm_regression_primal(y, x, c, epsilon=None)
¶
SVM regression, primal formulation.
Same c/epsilon semantics as svm_regression_dual except epsilon=0
is not a distinct primal branch in the original (only the LS-SVM
epsilon<0 case and the epsilon>0 case are formulated directly).
Original: svm/svm_regression_primal.m
Source code in src/quanttoolbox/svm/svm.py
291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 | |
Examples¶
Hard- and soft-margin SVM classification, dual formulation — svm/svm5.py
"""Translated from Examples/svm/svm5.m -- hard margin dual on the first
15 points, and soft margin dual (C=0.05) on the full 17-point dataset.
(The original's parameter sweep over C=0..0.4 for plotting purposes is
not re-translated separately -- svm3.py/svm4.py already demonstrate the
same sweep pattern at 4 representative C values.)"""
import numpy as np
from quanttoolbox.svm.svm import svm_classification_dual
data = np.array(
[
[0.5, 2.5, 1],
[2.7, 4.2, 1],
[2.7, 2.0, 1],
[1.7, 4.2, 1],
[1.5, 0.7, 1],
[2.3, 5.3, 1],
[4.0, 6.9, 1],
[6.4, 4.5, -1],
[7.7, 2.2, -1],
[8.8, 6.0, -1],
[7.4, 6.5, -1],
[6.5, 1.7, -1],
[8.3, 1.3, -1],
[6.0, 1.3, -1],
[5.0, 0.5, -1],
]
)
y15, x15 = data[:, 2], data[:, 0:2]
r_hard = svm_classification_dual(y15, x15, c=None)
print("hard margin: beta0=", round(r_hard.beta0, 6), "beta=", np.round(r_hard.beta, 6))
data17 = np.vstack([data, [[6.0, 5.0, 1], [2.0, 2.0, -1]]])
y17, x17 = data17[:, 2], data17[:, 0:2]
r_soft = svm_classification_dual(y17, x17, c=0.05, loss="hinge")
print("soft margin (C=0.05): beta0=", round(r_soft.beta0, 6), "beta=", np.round(r_soft.beta, 6))
Hard-margin SVM classification, dual formulation — svm/svm2.py
"""Translated from Examples/svm/svm2.m -- hard margin SVM classification,
dual formulation (same 15-point dataset as svm1.m)."""
import numpy as np
from quanttoolbox.svm.svm import svm_classification_dual
data = np.array(
[
[0.5, 2.5, 1],
[2.7, 4.2, 1],
[2.7, 2.0, 1],
[1.7, 4.2, 1],
[1.5, 0.7, 1],
[2.3, 5.3, 1],
[4.0, 6.9, 1],
[6.4, 4.5, -1],
[7.7, 2.2, -1],
[8.8, 6.0, -1],
[7.4, 6.5, -1],
[6.5, 1.7, -1],
[8.3, 1.3, -1],
[6.0, 1.3, -1],
[5.0, 0.5, -1],
]
)
y, x = data[:, 2], data[:, 0:2]
r = svm_classification_dual(y, x, c=None)
print("beta0:", round(r.beta0, 8))
print("beta:", np.round(r.beta, 8))
print("margin:", round(r.margin, 8))
print("support vectors (0-indexed):", r.support_vectors)
print("alpha at support vectors:", np.round(r.alpha[r.support_vectors], 8))
OLS, LAD, quantile, and SVM regression compared — svm/svm6.py
"""Translated from Examples/svm/svm6.m -- comparison of OLS, LAD, and
SVM-regression (LS-SVM and epsilon-SVM, at two different C values)
estimates on the same 10-obs dataset used in ols1.m/robust1.m."""
import numpy as np
from quanttoolbox.econometrics.estimation import ols_estimation
from quanttoolbox.stats.regression.quantile import quantile_regression
from quanttoolbox.stats.regression.robust import lad_regression
from quanttoolbox.svm.svm import svm_regression_primal
data = np.array(
[
[1.5, 1.0, 2.4, 3.6, 0.3],
[20.4, 1.0, 1.1, 3.8, 5.9],
[17.1, 1.0, 5.1, 6.3, 6.1],
[30.9, 1.0, 2.7, 2.4, 9.5],
[22.2, 1.0, 3.3, 3.0, 7.4],
[9.1, 1.0, 1.0, 5.4, 4.9],
[39.2, 1.0, 9.6, 2.8, 8.1],
[3.1, 1.0, 2.9, 4.4, 1.0],
[7.2, 1.0, 4.2, 5.6, 1.7],
[27.6, 1.0, 8.1, 1.7, 5.4],
]
)
y = data[:, 0]
x_full = data[:, 1:5]
beta_ols = ols_estimation(y, x_full).beta
beta_lad = lad_regression(y, x_full).beta
beta_lad2, _, _ = quantile_regression(y, x_full, tau=0.5)
x = x_full[:, 1:4] # drop the intercept and 4th column for SVM (3 predictors)
r_ls1 = svm_regression_primal(y, x, c=1)
r_eps1 = svm_regression_primal(y, x, c=1, epsilon=1)
r_ls2 = svm_regression_primal(y, x, c=100000)
r_eps2 = svm_regression_primal(y, x, c=100000, epsilon=0)
print("OLS:", np.round(beta_ols, 3))
print("LAD:", np.round(beta_lad, 3))
print("Quantile(0.5):", np.round(beta_lad2, 3))
print("SVM LS (C=1):", round(r_ls1.beta0, 3), np.round(r_ls1.beta, 3))
print("SVM eps (C=1, eps=1):", round(r_eps1.beta0, 3), np.round(r_eps1.beta, 3))
print("SVM LS (C=1e5):", round(r_ls2.beta0, 3), np.round(r_ls2.beta, 3))
print("SVM eps (C=1e5, eps=0):", round(r_eps2.beta0, 3), np.round(r_eps2.beta, 3))
OLS/SVM-LS and quantile/SVM-epsilon regression on synthetic data — svm/svm8.py
"""Translated from Examples/svm/svm8.m -- OLS, LAD, quantile, and SVM
regression (primal, at two very different C values, LS- and
epsilon-insensitive) compared on a larger (n=1000) simulated dataset.
The original draws x/beta/u from MATLAB's unseeded `rand`/`randn`; a fixed
seed (`np.random.default_rng(0)`) is substituted here for reproducibility
-- this is the fixed-seed substitution the tracker notes svm8.m as still
needing."""
import numpy as np
from quanttoolbox.econometrics.estimation import ols_estimation
from quanttoolbox.stats.regression.quantile import quantile_regression
from quanttoolbox.stats.regression.robust import lad_regression
from quanttoolbox.svm.svm import svm_regression_primal
rng = np.random.default_rng(0)
n, k = 1000, 4
x = rng.random((n, k))
beta_true = 5 * rng.random(k)
u = 0.20 * rng.standard_normal(n)
beta0_true = -3.0
y = beta0_true + x @ beta_true + u
x_design = np.column_stack([np.ones(n), x])
beta_ols = ols_estimation(y, x_design).beta
beta_lad = lad_regression(y, x_design).beta
beta_lad2, _, _ = quantile_regression(y, x_design, tau=0.5)
# SVM regression (primal), C=1
r_ls1 = svm_regression_primal(y, x, c=1) # LS-SVM
beta_svm_ls = np.concatenate([[r_ls1.beta0], r_ls1.beta])
r_eps1 = svm_regression_primal(y, x, c=1, epsilon=1) # epsilon-SVM
beta_svm_epsilon = np.concatenate([[r_eps1.beta0], r_eps1.beta])
# SVM regression (primal), C=1000 (effectively unregularized)
r_ls2 = svm_regression_primal(y, x, c=1000) # LS-SVM
beta_svm_ls2 = np.concatenate([[r_ls2.beta0], r_ls2.beta])
r_eps2 = svm_regression_primal(y, x, c=1000, epsilon=0) # epsilon-SVM, epsilon=0
beta_svm_epsilon2 = np.concatenate([[r_eps2.beta0], r_eps2.beta])
results = np.column_stack(
[beta_ols, beta_lad, beta_lad2, beta_svm_ls, beta_svm_epsilon, beta_svm_ls2, beta_svm_epsilon2]
)
print("Comparison of OLS, LAD, quantile(0.5), and SVM estimates")
print(
"columns: OLS, LAD, Quantile(0.5), SVM-LS(C=1), SVM-eps(C=1,eps=1), SVM-LS(C=1000), SVM-eps(C=1000,eps=0)"
)
print(np.round(results, 3))
print("\nOLS vs. SVM-LS at large C (expect near-identical -- unregularized limit):")
print(np.round(np.column_stack([beta_ols, beta_svm_ls2]), 3))
print(
"\nQuantile(0.5) vs. SVM-eps at large C, epsilon=0 (expect near-identical -- both approach LAD):"
)
print(np.round(np.column_stack([beta_lad2, beta_svm_epsilon2]), 3))
Soft-margin SVM classification, dual formulation — svm/svm4.py
"""Translated from Examples/svm/svm4.m -- soft margin SVM classification
(binary hinge loss), dual formulation, at 4 different C values. Same
17-point dataset as svm3.m."""
import numpy as np
from quanttoolbox.svm.svm import svm_classification_dual
data = np.array(
[
[0.5, 2.5, 1],
[2.7, 4.2, 1],
[2.7, 2.0, 1],
[1.7, 4.2, 1],
[1.5, 0.7, 1],
[2.3, 5.3, 1],
[4.0, 6.9, 1],
[6.4, 4.5, -1],
[7.7, 2.2, -1],
[8.8, 6.0, -1],
[7.4, 6.5, -1],
[6.5, 1.7, -1],
[8.3, 1.3, -1],
[6.0, 1.3, -1],
[5.0, 0.5, -1],
[6.0, 5.0, 1],
[2.0, 2.0, -1],
]
)
y, x = data[:, 2], data[:, 0:2]
for c in [0.01, 0.03, 0.05, 0.30]:
r = svm_classification_dual(y, x, c=c, loss="hinge")
print(f"C={c}: beta0={round(r.beta0,6)} beta={np.round(r.beta,6)} margin={round(r.margin,6)}")
print(f" support vectors: {r.support_vectors}")
Soft-margin SVM classification, primal formulation — svm/svm3.py
"""Translated from Examples/svm/svm3.m -- soft margin SVM classification
(binary hinge loss), primal formulation, at 4 different C values. 17-point
dataset (2 extra, harder-to-separate points added vs. svm1/svm2)."""
import numpy as np
from quanttoolbox.svm.svm import svm_classification_primal
data = np.array(
[
[0.5, 2.5, 1],
[2.7, 4.2, 1],
[2.7, 2.0, 1],
[1.7, 4.2, 1],
[1.5, 0.7, 1],
[2.3, 5.3, 1],
[4.0, 6.9, 1],
[6.4, 4.5, -1],
[7.7, 2.2, -1],
[8.8, 6.0, -1],
[7.4, 6.5, -1],
[6.5, 1.7, -1],
[8.3, 1.3, -1],
[6.0, 1.3, -1],
[5.0, 0.5, -1],
[6.0, 5.0, 1],
[2.0, 2.0, -1],
]
)
y, x = data[:, 2], data[:, 0:2]
for c in [0.01, 0.03, 0.05, 0.30]:
r = svm_classification_primal(y, x, c=c, loss="hinge")
print(f"C={c}: beta0={round(r.beta0,6)} beta={np.round(r.beta,6)} margin={round(r.margin,6)}")
SVM regression, dual formulation, vs. the primal result — svm/svm7.py
"""Translated from Examples/svm/svm7.m -- same comparison as svm6.m
(OLS/LAD/quantile vs SVM regression), but using the dual formulation
(svm_regression_dual) instead of primal."""
import numpy as np
from quanttoolbox.svm.svm import svm_regression_dual
data = np.array(
[
[1.5, 1.0, 2.4, 3.6, 0.3],
[20.4, 1.0, 1.1, 3.8, 5.9],
[17.1, 1.0, 5.1, 6.3, 6.1],
[30.9, 1.0, 2.7, 2.4, 9.5],
[22.2, 1.0, 3.3, 3.0, 7.4],
[9.1, 1.0, 1.0, 5.4, 4.9],
[39.2, 1.0, 9.6, 2.8, 8.1],
[3.1, 1.0, 2.9, 4.4, 1.0],
[7.2, 1.0, 4.2, 5.6, 1.7],
[27.6, 1.0, 8.1, 1.7, 5.4],
]
)
y = data[:, 0]
x_full = data[:, 1:5]
x = x_full[:, 1:4]
r_ls1 = svm_regression_dual(y, x, c=1)
r_eps1 = svm_regression_dual(y, x, c=1, epsilon=1)
r_ls2 = svm_regression_dual(y, x, c=100000)
r_eps2 = svm_regression_dual(y, x, c=100000, epsilon=0)
print("SVM dual LS (C=1):", round(r_ls1.beta0, 3), np.round(r_ls1.beta, 3))
print("SVM dual eps (C=1, eps=1):", round(r_eps1.beta0, 3), np.round(r_eps1.beta, 3))
print("SVM dual LS (C=1e5):", round(r_ls2.beta0, 3), np.round(r_ls2.beta, 3))
print("SVM dual eps (C=1e5, eps=0):", round(r_eps2.beta0, 3), np.round(r_eps2.beta, 3))
# expect these to match svm6.py's primal results exactly (strong duality)