Support Vector Machines and Support Vector Regression › Implementation in Scikit-learn

Michael BrenndoerferOctober 26, 20268 min read
Machine Learning from ScratchAll chapters
Part 7 of 15Supervised Learning Beyond Linear Models
Chapter 56Support Vector Machines and Support Vector Regression › Implementation in Scikit-learn
Reading section 7 of 10 · Implementation in Scikit-learn · 8 minSection 7 of 10 · 8 min

Section 7 of 10 · 8 min read

Support Vector Machines and Support Vector Regression › Implementation in Scikit-learnSection view · Sections in this chapter7 / 10 ↓
Reading tools Definitions, links & notes

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Notes & comments

We build the linear hinge-loss classifier from primitives first, then compare it with scikit-learn's SVC on the same data. Working from first principles helps us see where the subgradient comes from and how sensitive the solution is to feature scaling. We standardize the numerical inputs here so that both optimizers use the same feature representation.

PythonIn [16]
CollapseExpand
def hinge_objective(X, y_signed, w, b, C):
    margins = y_signed * (X @ w + b)
    return 0.5 * (w @ w) + C * np.maximum(0.0, 1.0 - margins).sum()


def hinge_subgradient(X, y_signed, w, b, C):
    margins = y_signed * (X @ w + b)
    active = (1.0 - margins) > 0
    dw = w - C * (X[active].T @ y_signed[active])
    db = -C * y_signed[active].sum()
    return dw, db


def train_linear_svm(X, y_signed, C=1.0, lr=0.05, n_iter=3000):
    w = np.zeros(X.shape[1])
    b = 0.0
    for _ in range(n_iter):
        dw, db = hinge_subgradient(X, y_signed, w, b, C)
        w = w - lr * dw
        b = b - lr * db
    return w, b

We reuse the toy two-dimensional dataset from the visualization section. The full toy set acts as training data here; this example has no held-out performance estimate. We compute the means and standard deviations from those training inputs only, then supply the same standardized features to both optimizers. Scaling changes the geometry of the weight penalty and kernel distances. A feature with large numerical units can contribute heavily to a distance calculation, but that does not imply that an SVM must ignore every smaller-scale feature.

PythonIn [17]
CollapseExpand
mu = X_toy.mean(axis=0)
sd = X_toy.std(axis=0)
X_std = (X_toy - mu) / sd
y_signed = np.where(y_toy == 1, 1.0, -1.0)

w_scratch, b_scratch = train_linear_svm(
    X_std, y_signed, C=2.0, lr=0.05, n_iter=3000
)
train_scores = X_std @ w_scratch + b_scratch
train_pred = (train_scores > 0).astype(int)
margins_scratch = y_signed * (X_std @ w_scratch + b_scratch)
n_active = int(((1.0 - margins_scratch) > 0).sum())
train_accuracy_scratch = (train_pred == y_toy).mean()

This fixed-step subgradient run uses a budget of 3,000 updates; that budget does not certify convergence to the optimum. We print the training accuracy, the weight norm ∥w∥\|\mathbf{w}\|, and the count of hinge-active examples. The count records strict margin violations, mi<1m_i<1, at the current iterate. It includes misclassified points as well as correctly classified points too close to the boundary.

ConsoleOut [18]
CollapseExpand
scratch training accuracy: 1.000
scratch ||w||:              1.228
hinge-active examples:      0/40
hinge-active fraction:      0.000

Now compare with the reference SVC, using the same CC and standardized inputs. Its printed margin width is measured in the standardized feature coordinates supplied to this fit. The reference solver works with the dual quadratic program, while our scratch implementation descends on the primal hinge objective. They target the same normalized hinge objective, but matching training labels alone does not establish that their parameter estimates or objective values agree.

PythonIn [19]
CollapseExpand
svc_ref = SVC(kernel="linear", C=2.0).fit(X_std, y_toy)
svc_pred = svc_ref.predict(X_std)
svc_margin = 2.0 / np.linalg.norm(svc_ref.coef_.ravel())
scratch_objective = hinge_objective(
    X_std, y_signed, w_scratch, b_scratch, C=2.0
)
reference_objective = hinge_objective(
    X_std, y_signed, svc_ref.coef_.ravel(), svc_ref.intercept_[0], C=2.0
)
objective_gap = scratch_objective - reference_objective
reference_support_count = svc_ref.support_.size
ConsoleOut [20]
CollapseExpand
sklearn training accuracy: 1.000
sklearn ||w||:             1.142
sklearn margin width:      1.752
agreement (scratch, sklearn): 1.000
sklearn support vectors:   2/40
sklearn support fraction:  0.050
scratch hinge objective:   0.754472
reference hinge objective: 0.651577
objective gap:             0.102895

The printed label agreement and objective gap answer different questions. Matching labels on this easy training set can coexist with a noticeable objective gap, because a fixed step size can keep the scratch iterate moving around the nonsmooth optimum. The scratch objective uses 12∥w∥2+C∑iξi\tfrac{1}{2}\|\mathbf{w}\|^2 + C\sum_i \xi_i, matching the reference SVC normalization. The scratch hinge-active fraction counts strict margin violations at its current iterate; it is not the dual support-vector fraction. The reference support count comes separately from svc_ref.support_.

The SVR side follows the same pattern. We reuse the noisy sine dataset from the visualization and inspect epsilon-insensitive loss and tube occupancy alongside the reference support-vector set. Points strictly inside the tube have zero epsilon-insensitive loss and zero dual coefficients. Points on the tube edges can have nonzero coefficients, as can points outside; the support set is not limited to strict outside points.

PythonIn [21]
CollapseExpand
def epsilon_insensitive_loss(residuals, epsilon):
    return np.maximum(0.0, np.abs(residuals) - epsilon)


svr_epsilon = 0.2
svr_ref = SVR(kernel="rbf", C=2.0, gamma=0.9, epsilon=svr_epsilon).fit(
    X_demo, y_demo
)
in_sample_pred = svr_ref.predict(X_demo)
res = y_demo - in_sample_pred
inside_tube = np.abs(res) <= svr_epsilon
inside_count = int(inside_tube.sum())
inside_fraction = inside_tube.mean()
mean_epsilon_loss = epsilon_insensitive_loss(res, svr_epsilon).mean()
mean_squared_residual = (res**2).mean()
ConsoleOut [22]
CollapseExpand
SVR support-vector count:  8/60
mean epsilon-insensitive loss: 0.0000
mean squared residual:         0.0114
points with |residual| <= epsilon: 55/60 (0.917)

The printed tube occupancy directly counts residuals with ∣r∣≤ε|r| \leq \varepsilon, using the fitted predictions. The mean epsilon-insensitive loss and mean squared residual summarize different loss functions and scale differently when target units change; their relative magnitudes do not reveal how many observations lie inside the tube. The reference support set can include points on the tube boundary, and finite solver tolerance can put their computed residuals slightly inside or outside it. These in-sample diagnostics describe the fit; they do not establish that the model generalizes or that a particular tube width is optimal.

The tube width is a tuning choice, not a fixed property of the data. Refitting SVR on the same sine training set over a small epsilon grid shows how the support fraction and the fraction of in-sample residuals inside the tube vary. Here the support fraction decreases across the grid, while the measured inside-tube fraction rises overall and is unchanged between epsilon 0.2 and 0.4. These counts use the fitted residuals and the exact comparison shown in the code, so solver tolerance can affect points near the tube edges. This is a training-set diagnostic, not a validation comparison.

PythonIn [23]
CollapseExpand
epsilon_grid = np.array([0.05, 0.10, 0.20, 0.40, 0.80])
support_fraction = []
inside_fraction = []
for eps_val in epsilon_grid:
    svr_scan = SVR(kernel="rbf", C=2.0, gamma=0.9, epsilon=float(eps_val)).fit(
        X_demo, y_demo
    )
    pred_scan = svr_scan.predict(X_demo)
    support_fraction.append(len(svr_scan.support_) / len(y_demo))
    inside_fraction.append(np.mean(np.abs(y_demo - pred_scan) <= eps_val))
support_fraction = np.array(support_fraction)
inside_fraction = np.array(inside_fraction)
VisualizationOut [24]
CollapseExpand
Support-vector fraction decreases across five epsilon fits; the inside-tube fraction rises overall and is equal at epsilon 0.2 and 0.4.
Support-vector fraction and in-sample inside-tube fraction for RBF SVR as the epsilon tube half-width varies on a fixed training set. On this seeded grid, the support fraction decreases and the measured inside-tube fraction rises overall, with equal inside fractions at epsilon 0.2 and 0.4. Each point uses a refitted model at the same C and gamma; these observed trends are not a universal monotonicity guarantee.

The implementation uses C, kernel, gamma, and epsilon directly; these are summarized jointly with the reference-library controls in the Key Methods and Key Parameters sections that follow.

Key Methods

These methods support fitting, prediction, and inspection of the decision scores.

  • SVC(...).fit(X, y): trains a soft-margin classifier. The linear-kernel version can be compared with dedicated linear solvers for larger datasets.
  • SVR(...).fit(X, y): trains an ε\varepsilon-insensitive regressor, using C, the selected kernel's settings, and epsilon. The class_weight parameter belongs to SVC; both estimators accept sample_weight in fit to rescale individual violation penalties.
  • decision_function(X): in the binary example, returns the score f(x)f(\mathbf{x}). Positive scores select the positive class. For a nonzero linear weight vector, divide by ∥w∥\|\mathbf{w}\| to obtain signed perpendicular distance. For multiclass SVC, training uses one-vs-one classifiers; the default decision_function_shape="ovr" converts their outputs into one score per class. Scores are not calibrated probabilities.
  • predict(X): returns class labels for SVC and continuous predictions for SVR.
  • predict_proba(X): for SVC, enable probability=True before fitting. libsvm estimates probabilities using additional internal five-fold cross-validation and Platt scaling for the binary models. This slows fitting, and the most probable class can differ from predict(X). Assess probability calibration on held-out data.

Key Parameters

Below are some of the main parameters that affect how SVMs work and perform, using SVC and SVR in scikit-learn.

  • C: controls the penalty for margin or tube violations. Under the mean-loss convention λ∥w∥2/2+∑iLi/n\lambda\|\mathbf{w}\|^2/2+\sum_iL_i/n, C=1/(nλ)C=1/(n\lambda); larger C means weaker relative regularization.
  • kernel: chooses the feature-space inner product. A linear kernel keeps a linear boundary in the supplied features; polynomial and RBF kernels can produce nonlinear boundaries.
  • gamma: controls RBF locality, with characteristic distance 1/γ1/\sqrt{\gamma}. Tune it jointly with C; taking it to zero at fixed C does not generally recover a linear SVM.
  • epsilon: sets the SVR tube's half-width in target units. A wider tube ignores more residual values for a fixed fitted function and can encourage a sparser fit. At epsilon=0 the loss is absolute error, rather than squared error.
  • class_weight: changes the class-specific violation penalties in SVC. Choose weights using class frequencies, error costs, and validation; weighting and resampling are options whose suitability depends on the task.

Comments

Add to the conversation

A question, a correction, or an example from your own work.

No comments yet. Be the first to share your thoughts!

Newsletter

Stay up to date

Get new articles, book updates, and notes on AI and technology.

Unsubscribe at any time. Privacy policy