Skip to content

The Soft Margin SVM

Page 1203 built the hard margin SVM and page 1202 showed how it fails: move one negative point to (3.5,0)(3.5, 0) and there is no feasible solution at all. Real data is not separable, and a method that returns nothing is not a method.

Section 12.2.4’s repair is one new variable per example.

Give each example–label pair (xn,yn)(\mathbf{x}_n, y_n) a slack variable ξn0\xi_n \geq 0, subtract it from the margin requirement, and charge for it in the objective:

minw,b,ξ 12w2+Cn=1Nξn(12.26a)\min_{\mathbf{w}, b, \boldsymbol\xi}\ \tfrac12\lVert\mathbf{w}\rVert^2 + C\sum_{n=1}^{N}\xi_n \qquad \text{(12.26a)} subject toyn(w,xn+b)1ξn,ξn0(12.26b), (12.26c)\text{subject to}\quad y_n(\langle\mathbf{w},\mathbf{x}_n\rangle + b) \geq 1 - \xi_n, \qquad \xi_n \geq 0 \qquad \text{(12.26b), (12.26c)}

ξn\xi_n is how far example nn falls short of its own margin, measured in the units where the margin is 11. Three regimes, and the book names only two of them:

ξn\xi_nwhere the example is
ξn=0\xi_n = 0correctly classified, at or beyond its margin — it costs nothing
0<ξn10 < \xi_n \leq 1correct side of the hyperplane, but inside the margin
ξn>1\xi_n > 1wrong side of the hyperplane

ξn=1\xi_n = 1 is exactly the boundary between the last two, because ξn=1ynf(xn)\xi_n = 1 - y_n f(\mathbf{x}_n) at the optimum and ynf(xn)=0y_n f(\mathbf{x}_n) = 0 means the example sits on the hyperplane.

figure Equation 12.26: the slack each example needs to reach its own margin matplotlib
Three side-by-side plots of the same nine points at C equal to 0.1, 1 and 10. Each shows a solid black decision boundary with amber and blue dashed margin lines on either side; as C rises the margin band narrows sharply. Red arrows run from each violating example to the margin line it fails to reach — three arrows in the first panel, three in the second, one in the third. Three side-by-side plots of the same nine points at C equal to 0.1, 1 and 10. Each shows a solid black decision boundary with amber and blue dashed margin lines on either side; as C rises the margin band narrows sharply. Red arrows run from each violating example to the margin line it fails to reach — three arrows in the first panel, three in the second, one in the third.
The red arrow on each violating point is its ξ, drawn as the distance it would have to travel to reach its own margin. At C = 0.1 the margin is wide and three examples pay for it; at C = 10 the margin has shrunk to 1 and only the intruder pays.

The data below is page 1202’s impossible case: the running example plus a negative point at (4.5,0)(4.5, 0), which is the average of (3,1)(3, 1) and (6,1)(6, -1) and therefore sits strictly inside the positive class’s convex hull. No hyperplane can classify it correctly. The hard margin problem has no answer; the soft margin problem always has one.

setup.py
import numpy as np
from scipy.optimize import minimize
 
X = np.array([[3.0,  1.0], [3.0, -1.0], [6.0,  1.0], [6.0, -1.0],
              [1.0,  0.0], [0.0,  1.0], [0.0, -1.0], [-1.0, 0.0]])
Y = np.array([ 1.0,  1.0,  1.0,  1.0,  -1.0, -1.0, -1.0, -1.0])
XB = np.vstack([X, [4.5, 0.0]])          # the intruder
YB = np.r_[Y, -1.0]
 
def pred(scores):
    """Section 12.1's rule: f(x) >= 0 is class +1, ties included."""
    return np.where(scores >= 0.0, 1.0, -1.0)
 
def solve(XX, YY, C, seed):
    n, d = XX.shape
    rg = np.random.default_rng(seed)
    v0 = np.r_[rg.normal(0, 1, d), rg.normal(0, 1),
               np.abs(rg.normal(1, 0.5, n))]
    return minimize(lambda v: 0.5 * v[:d] @ v[:d]
                    + C * np.maximum(v[d + 1:], 0.0).sum(), v0,
                    constraints=[
                        {"type": "ineq",
                         "fun": lambda v: YY * (XX @ v[:d] + v[d]) - 1 + v[d + 1:]},
                        {"type": "ineq", "fun": lambda v: v[d + 1:]}],
                    method="SLSQP", options={"maxiter": 20000, "ftol": 1e-13})
 
def best(XX, YY, C, tries=30):
    out, bv = None, np.inf
    d = XX.shape[1]
    for s in range(tries):
        r = solve(XX, YY, C, s)
        if not r.success:
            continue
        w, b = r.x[:d], r.x[d]
        xi = np.maximum(0.0, 1 - YY * (XX @ w + b))   # the tight slack
        val = 0.5 * w @ w + C * xi.sum()
        if val < bv - 1e-12:
            bv, out = val, (w.copy(), float(b), xi)
    return out
the_c_sweep.py
print(f"{'C':>9} {'margin':>9} {'sum xi':>9} {'xi>0':>6} {'xi>1':>6} "
      f"{'wrong':>6} {'0.5||w||^2':>12} {'objective':>12}")
for C in (0.01, 0.1, 0.5, 1.0, 2.0, 10.0, 100.0, 1000.0):
    w, b, xi = best(XB, YB, C)
    nw = np.linalg.norm(w)
    print(f"{C:>9.2f} {1/nw:>9.4f} {xi.sum():>9.4f} "
          f"{int((xi > 1e-6).sum()):>6} {int((xi > 1 + 1e-6).sum()):>6} "
          f"{int((pred(XB @ w + b) != YB).sum()):>6} "
          f"{0.5*nw**2:>12.4f} {0.5*nw**2 + C*xi.sum():>12.4f}")
text
        C    margin    sum xi   xi>0   xi>1  wrong   0.5||w||^2    objective
     0.01    8.0000    6.4375      8      4      4       0.0078       0.0722
     0.10    3.0000    3.8333      4      1      1       0.0556       0.4389
     0.50    3.0000    3.8333      4      1      1       0.0556       1.9722
     1.00    2.0000    3.7500      4      1      1       0.1250       3.8750
     2.00    1.0000    3.5000      1      1      1       0.5000       7.5000
    10.00    1.0000    3.5000      1      1      1       0.5000      35.5000
   100.00    1.0000    3.5000      1      1      1       0.5000     350.5000
  1000.00    1.0000    3.5000      1      1      1       0.5000    3500.5000

Read the xi>0 and xi>1 columns against each other. At C=0.01C = 0.01 every one of the nine examples is paying slack and four are on the wrong side; at C=10C = 10 only the intruder pays anything at all. The book’s phrase “within the margin or even on the wrong side” covers two populations that move independently, and the count of violated margins is not the count of errors.

One training error is the floor, not a failure. The intruder is inside the positive hull, so no hyperplane of any kind gets it right. What CC buys above C=2C = 2 is nothing: the solution has already stopped moving.

figure C is the price of a violation, and it buys margin at the cost of accuracy matplotlib
Left, a log-log plot against C from ten to the minus four to one thousand: a blue margin curve falling steeply from eight hundred and flattening at one, and a red total-slack curve falling gently from eight and flattening near three point five. Right, a step plot of training errors against C on a log axis, dropping from four to three to one and then flat at one, with a dashed green line at one annotated as the floor. Left, a log-log plot against C from ten to the minus four to one thousand: a blue margin curve falling steeply from eight hundred and flattening at one, and a red total-slack curve falling gently from eight and flattening near three point five. Right, a step plot of training errors against C on a log axis, dropping from four to three to one and then flat at one, with a dashed green line at one annotated as the floor.
Both curves flatten above C ≈ 2. Past that point the constraint set is already satisfied as tightly as it can be and raising C only scales the objective — the last three rows of the table differ by a factor of ten in objective and not at all in solution.

As CC \to \infty the slack becomes infinitely expensive, so on separable data the solver is forced back to the hard margin answer:

c_to_infinity.py
W_STAR, B_STAR = np.array([1.0, 0.0]), -2.0
print(f"{'C':>9} {'margin':>11} {'sum xi':>11} {'||w - w*||':>12} {'|b - b*|':>11}")
for C in (0.1, 0.5, 1.0, 5.0, 50.0, 500.0, 5000.0):
    w, b, xi = best(X, Y, C)               # the SEPARABLE running example
    print(f"{C:>9.1f} {1/np.linalg.norm(w):>11.6f} {xi.sum():>11.3e} "
          f"{np.linalg.norm(w - W_STAR):>12.3e} {abs(b - B_STAR):>11.3e}")
text
        C      margin      sum xi   ||w - w*||    |b - b*|
      0.1    2.000000   1.500e+00    5.000e-01   1.000e+00
      0.5    1.000000   2.139e-08    1.069e-08   3.208e-08
      1.0    1.000000   1.332e-15    5.334e-16   0.000e+00
      5.0    1.000000   6.217e-14    3.900e-15   2.043e-14
     50.0    1.000000   3.997e-15    4.415e-15   1.110e-14
    500.0    1.000000   0.000e+00    6.736e-13   7.665e-13
   5000.0    1.000000   0.000e+00    1.415e-15   2.220e-15

It does not take a large CCby C=1C = 1 the recovery is exact, and bb|b - b^*| is literally zero. On separable data the hard margin solution is already feasible with zero slack, so any CC above a threshold reproduces it and there is nothing further to gain.

As C0C \to 0 the opposite happens: slack is free, weight is not, and the model gives up.

text
        C       ||w||      margin     sum xi  wrong
    1e-04    0.001250    800.0057     7.9844      4
    1e-03    0.012500     80.0001     7.8438      4
    1e-02    0.125000      8.0000     6.4375      4
    1e-01    0.333333      3.0000     3.8333      1
    2e-01    0.333333      3.0000     3.8333      1

w\lVert\mathbf{w}\rVert falls in proportion to CC and the margin balloons to 800800 — a “margin” that contains the entire dataset and means nothing. At C=0C = 0 exactly, the optimum is w=0\mathbf{w} = \mathbf{0} and the classifier is constant. So CC is not a free knob at one end: it is a regularisation parameter and the book names it so, with the warning that its direction is the reverse of the usual convention — “a large value of CC implies low regularization”, because CC multiplies the error term rather than the regulariser.

The book adds, in small type:

In the formulation of the soft margin SVM (12.26a) w\mathbf{w} is regularized, but bb is not regularized. We can see this by observing that the regularization term does not contain bb. The unregularized term bb complicates theoretical analysis and decreases computational efficiency.

That reads as a minor inconvenience. It is the opposite — leaving bb out is what makes the method invariant to where you put the origin. Shift every example by a vector t\mathbf{t} and the pair (w,bw,t)(\mathbf{w}, b - \langle\mathbf{w},\mathbf{t}\rangle) is feasible with the same slacks and the same 12w2\tfrac12\lVert\mathbf{w}\rVert^2, so the optimal value cannot change. Measured, with t=(10,0)\mathbf{t} = (10, 0):

CCbb penalisedwnewwold\lVert\mathbf{w}_{\text{new}} - \mathbf{w}_{\text{old}}\rVertbnew(boldw,t)\lvert b_{\text{new}} - (b_{\text{old}} - \langle\mathbf{w},\mathbf{t}\rangle)\rvertchange in objective
0.10.1no1.993×10151.993\times10^{-15}7.105×10157.105\times10^{-15}9.437×10169.437\times10^{-16}
0.10.1yes1.450×1011.450\times10^{-1}2.3202.3202.272×1012.272\times10^{-1}
1.01.0no1.039×1081.039\times10^{-8}9.662×1039.662\times10^{-3}8.882×10168.882\times10^{-16}
1.01.0yes3.619×1013.619\times10^{-1}4.7904.7903.6733.673
10.010.0no9.812×10159.812\times10^{-15}1.332×10131.332\times10^{-13}9.948×10149.948\times10^{-14}
10.010.0yes4.667×1014.667\times10^{-1}5.0675.06710.48\mathbf{10.48}

With bb free the objective value is unchanged to machine precision at every CC, and w\mathbf{w} does not move. Add 12b2\tfrac12 b^2 to the objective and translating the data changes the answer — at C=10C = 10 the optimal value moves by 10.4810.48 and w\lVert\mathbf{w}\rVert drops from 1.0000001.000000 to 0.8000000.800000. The penalised model has been told that a boundary near the origin is cheaper, and the origin is wherever somebody happened to put it.

pch.quizTag Check your understanding
  1. pch.quizShowAnswer

    C — On the correct side of the hyperplane but inside the margin — since xi = 1 - y f(x), a value below 1 means y f(x) is still positive

  2. pch.quizShowAnswer

    B — By C = 1, where |b - b*| is exactly zero — the hard margin solution is already feasible with zero slack, so no further increase changes anything

  3. pch.quizShowAnswer

    B — No — the norm of w collapses in proportion to C, so the 'margin' 1/||w|| balloons while containing the whole dataset. At C = 0 exactly the optimum is w = 0 and the classifier is constant

  4. pch.quizShowAnswer

    C — Because penalising b would make the solution depend on where the origin sits — measured, shifting the data by (10, 0) leaves the objective unchanged to 1e-15 with b free, and changes it by 10.48 with b penalised

Exercise 1 – Classify the slack into three regimes

Section titled “Exercise 1 – Classify the slack into three regimes”

Exercise 2 – Recover the hard margin by raising C

Section titled “Exercise 2 – Recover the hard margin by raising C”

Exercise 3 – Translate the data and see what moves

Section titled “Exercise 3 – Translate the data and see what moves”
  • Real data is not separable, and page 1203’s hard margin problem simply has no answer on it. One negative point inside the positive class’s convex hull is enough.
  • Section 12.2.4’s repair is one slack variable per example, subtracted from that example’s margin requirement and charged for in the objective.
  • The slack splits into three regimes, and the book names two. Zero means the example costs nothing, between zero and one means inside the margin, above one means on the wrong side — because xi equals one exactly when the example sits on the hyperplane.
  • The count of violated margins is not the count of errors. At C = 0.01 all nine examples pay slack and four are wrong; at C = 10 only one pays and one is wrong.
  • C is the price of a violation, and a large C means low regularisation — the reverse of the usual convention, because C multiplies the error term rather than the regulariser.
  • Above C = 2 on this data the solution stops moving entirely. The objective grows by a factor of ten across the last three rows and the parameters do not change at all.
  • On separable data any large enough C recovers the hard margin exactly — by C = 1 the intercept matches to zero and the weights to 5.3e-16.
  • As C tends to zero the weights collapse in proportion to it and the margin balloons to 800 while containing the whole dataset. At C = 0 the optimum is the constant classifier.
  • Near a degenerate optimum the training-error count is not reproducible. At C = 0.05 the boundary runs exactly through two training points and their labels are decided by a quantity of size 1e-15.
  • Leaving b out of the regulariser is what makes the method invariant to translating the data. Shifting by ten leaves the objective unchanged to 1e-15; penalising b changes it by 10.48 and shrinks the weights from 1.0 to 0.8.
  • The origin is wherever somebody put it, and a model that prefers boundaries near it is answering a question about the coordinate system.

Next: The Hinge Loss — §12.2.5, the same optimisation problem written without any constraints at all.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading