munotes®

Practical 5: Support Vector Machines

Get access to whole semester resourcesSemester Pass

Chapter Eight

Syllabus topic Module 1, "Support Vector Machines (SVM): Implement the SVM algorithm for binary classification. Train an SVM model using a given dataset and optimize its parameters. Evaluate the performance of the SVM model on test data and analyze the results."

Pages 62 to 68 of 206

Aim

To implement a Support Vector Machine for binary classification, to train it on a dataset, to tune its parameters, and to measure it on test data.

What you need to know before you start

Many straight lines separate two classes. An SVM asks a sharper question: which line leaves the widest empty corridor between them?

Write the line as

w1x1 + w2x2 + b = 0

Give one class the label +1 and the other -1. Then for a point x with label y, the quantity

margin = y * (w.x + b)

is positive when the point is on the correct side and negative when it is not, and its size says how far from the line the point is. The margin of the whole dataset is the smallest of those, and the SVM's job is to make it as large as possible.

Two ideas follow, and they are the two halves of the SVM.

The support vectors. Only the points closest to the line matter. Move a point that is far away and the best line does not change at all; move a support vector and it does. That is why the model is called a support vector machine, and it is why it is compact: a trained SVM is defined by a handful of points.

The soft margin, and C. Real data has points on the wrong side. Insisting on a perfect corridor then gives no answer at all, so the SVM is allowed to violate the margin and charged for it. C is the price. A small C buys a wide corridor and tolerates mistakes; a large C insists on getting the training points right and accepts a narrow corridor. C is the parameter MU means by "optimize its parameters", and it is chosen by measuring, not by taste.

What the program below minimises is the two of them added together:

(1/2)|w|^2 + C sum over the rows of max(0, 1 - y*(w.x + b))

The first term is small when the corridor is wide. The second, the hinge loss, is zero for a point comfortably outside the corridor and grows for a point inside it or on the wrong side.

Step 1: the SVM from nothing

Follow the slope of that expression downhill: for each row, if its margin is below 1, push w and b towards fixing it; if not, only shrink w. Take smaller steps as training goes on so that it settles.

name,attendance,practice,result
Aarav,50,22,Fail
Isha,48,16,Fail
Rohan,88,38,Pass
Sanya,97,39,Pass
Vikram,90,30,Fail
Meera,35,41,Fail
Farhan,45,9,Fail
Nikita,71,8,Fail
Omkar,92,2,Fail
Pooja,97,45,Pass
Rahul,75,15,Fail
Sneha,85,18,Pass
Tejas,79,24,Pass
Urmila,83,34,Pass
Varun,44,23,Fail
Yash,46,37,Pass
Zoya,72,20,Fail
Amit,44,9,Pass
Bhavna,74,3,Fail
Chirag,82,25,Pass
Deepa,94,29,Pass
Eshan,46,27,Fail
Gauri,98,9,Fail
Harsh,89,34,Pass
Ira,97,27,Pass
Jatin,68,28,Pass
Kavya,96,34,Pass
Lalit,38,38,Fail
Manav,63,10,Fail
Neha,41,35,Fail
"""Practical 5: a linear soft-margin SVM trained by sub-gradient descent."""
import csv
import random

with open("students.csv", newline="") as fh:
    rows = list(csv.DictReader(fh))
raw = [[float(r["attendance"]), float(r["practice"])] for r in rows]
y = [1 if r["result"] == "Pass" else -1 for r in rows]

lo = [min(r[k] for r in raw) for k in range(2)]
hi = [max(r[k] for r in raw) for k in range(2)]
X = [[(r[k] - lo[k]) / (hi[k] - lo[k]) for k in range(2)] for r in raw]

random.seed(1)
order = list(range(len(X)))
random.shuffle(order)
cut = int(0.7 * len(X))
train, test = order[:cut], order[cut:]

def train_svm(idx, C, epochs=3000, rate0=0.5, seed=1):
    """Minimise (1/2)|w|^2 + C * sum(max(0, 1 - y(w.x + b))) by sub-gradient steps."""
    w = [0.0, 0.0]
    b = 0.0
    rng = random.Random(seed)
    n = len(idx)
    for epoch in range(1, epochs + 1):
        rate = rate0 / epoch          # a step that shrinks, so it settles
        shuffled = list(idx)
        rng.shuffle(shuffled)
        for i in shuffled:
            margin = y[i] * (w[0] * X[i][0] + w[1] * X[i][1] + b)
            if margin < 1:            # inside the margin or wrong side: push
                for k in range(2):
                    w[k] -= rate * (w[k] / n - C * y[i] * X[i][k])
                b += rate * C * y[i]
            else:                     # outside: only the shrink term applies
                for k in range(2):
                    w[k] -= rate * (w[k] / n)
    return w, b

def predict(w, b, x):
    return 1 if w[0] * x[0] + w[1] * x[1] + b >= 0 else -1

def accuracy(w, b, idx):
    return sum(1 for i in idx if predict(w, b, X[i]) == y[i]) / len(idx)

print("training rows %d, test rows %d" % (len(train), len(test)))
print()
print("%-8s %10s %10s %10s %10s %14s"
      % ("C", "w1", "w2", "b", "train acc", "test acc"))
best = None
for C in (0.01, 0.1, 1.0, 10.0, 100.0):
    w, b = train_svm(train, C)
    tr, te = accuracy(w, b, train), accuracy(w, b, test)
    print("%-8s %10.4f %10.4f %10.4f %10.3f %14.3f" % (C, w[0], w[1], b, tr, te))
    if best is None or te > best[0]:
        best = (te, C, w, b)
print()
te, C, w, b = best
print("best C on the test set: %s" % C)
print("decision boundary: %.4f * attendance_scaled + %.4f * practice_scaled + %.4f = 0"
      % (w[0], w[1], b))
print()
margins = sorted((y[i] * (w[0] * X[i][0] + w[1] * X[i][1] + b), i) for i in train)
print("the five training points closest to the boundary (the support vectors):")
print("%-10s %12s %10s %8s" % ("student", "attendance", "practice", "margin"))
for m, i in margins[:5]:
    print("%-10s %12s %10s %8.3f"
          % (rows[i]["name"], rows[i]["attendance"], rows[i]["practice"], m))
munotes.in62

Practical 5: Support Vector Machines

training rows 21, test rows 9

C                w1         w2          b  train acc       test acc
0.01         0.0260     0.0053    -0.1288      0.571          0.444
0.1          0.2791     0.1090    -1.0931      0.571          0.444
1.0          2.2230     1.1924    -2.0817      0.905          0.667
10.0         3.0884     2.3295    -3.0884      0.905          0.667
100.0        3.2274     2.4287    -3.1966      0.905          0.667

best C on the test set: 1.0
decision boundary: 2.2230 * attendance_scaled + 1.1924 * practice_scaled + -2.0817 = 0

the five training points closest to the boundary (the support vectors):
student      attendance   practice   margin
Amit                 44          9   -1.570
Gauri                98          9   -0.335
Tejas                79         24    0.081
Sneha                85         18    0.126
Chirag               82         25    0.215
munotes.in63

Practical 5: Support Vector Machines

C matters, and the table says by how much. At C = 0.01 the penalty for a mistake is so small that the model gives up and gets 0.571 on the training rows, which is barely better than guessing. From C = 1 upward it reaches 0.905 training and 0.667 test, and the weights stop changing much.

The five nearest points are the support vectors, and look at the first two: Amit, with 44 per cent attendance and 9 hours of practice, who passed, and Gauri, with 98 per cent attendance and 9 hours, who failed. Those are the two students the dataset was built around, the exceptions to its own rule, and they carry margins of -1.570 and -0.335, both negative, meaning both are on the wrong side of the line. The SVM has decided to accept those two mistakes in exchange for a wider corridor everywhere else. That is the soft margin doing its job, and it is the sentence to write in the journal.

Step 2: the picture

Thirty students plotted by scaled attendance and scaled practice hours, with the fitted line and the two margin lines.

Figure 8.1 All thirty students. The solid line is the boundary scikit-learn's linear SVC found on the twenty-one training rows with C = 1; the dashed lines are one unit of margin either side of it.

Reading it is the analysis MU asks for. The corridor runs from the lower left to the upper right, the Pass students sit mostly to the upper right of it, and the handful of filled circles on the wrong side and hollow squares on the right side are the errors the soft margin bought the corridor with.

Step 3: the same model from scikit-learn, and the kernels

A straight line is not always enough. The kernel trick lets an SVM draw a curved boundary without ever computing the curve: it measures similarity between points with a different function, and a straight line in that space of similarities is a curve in the original one. Three kernels are worth knowing, and MU's theory paper names them.

  • linear: the straight line above.
  • rbf, the radial basis function: similarity falls off with distance, so the boundary can be any smooth shape. The default in scikit-learn.
  • poly: a polynomial of the given degree, 3 by default.
munotes.in64

Practical 5: Support Vector Machines

"""The same job from scikit-learn: SVC, the kernels, and tuning C."""
import csv
from sklearn.svm import SVC
from sklearn.preprocessing import MinMaxScaler
from sklearn.model_selection import train_test_split

with open("students.csv", newline="") as fh:
    rows = list(csv.DictReader(fh))
X = [[float(r["attendance"]), float(r["practice"])] for r in rows]
y = [r["result"] for r in rows]
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3, random_state=1, stratify=y)
scaler = MinMaxScaler().fit(Xtr)
Str, Ste = scaler.transform(Xtr), scaler.transform(Xte)

print("%-10s %8s %10s %10s %16s" % ("kernel", "C", "train acc", "test acc", "support vectors"))
for kernel in ("linear", "rbf", "poly"):
    for C in (0.1, 1.0, 10.0):
        m = SVC(kernel=kernel, C=C, gamma="scale", degree=3, random_state=0).fit(Str, ytr)
        print("%-10s %8s %10.3f %10.3f %16d"
              % (kernel, C, m.score(Str, ytr), m.score(Ste, yte), len(m.support_)))

print()
m = SVC(kernel="linear", C=1.0).fit(Str, ytr)
print("linear, C=1: w = [%.4f, %.4f]  b = %.4f"
      % (m.coef_[0][0], m.coef_[0][1], m.intercept_[0]))
print("support vectors:", len(m.support_), "of", len(Str), "training rows")

print()
print("unscaled, to show it matters:")
for kernel in ("linear", "rbf"):
    m = SVC(kernel=kernel, C=1.0, gamma="scale").fit(Xtr, ytr)
    print("  %-8s train %.3f  test %.3f" % (kernel, m.score(Xtr, ytr), m.score(Xte, yte)))
kernel            C  train acc   test acc  support vectors
linear          0.1      0.524      0.556               20
linear          1.0      0.810      0.889               18
linear         10.0      0.810      0.889               13
rbf             0.1      0.524      0.556               21
rbf             1.0      0.857      0.889               15
rbf            10.0      0.905      0.889               14
poly            0.1      0.762      0.889               12
poly            1.0      0.857      1.000               12
poly           10.0      0.810      1.000               11

linear, C=1: w = [2.1774, 1.2564]  b = -2.0961
support vectors: 18 of 21 training rows

unscaled, to show it matters:
  linear   train 0.810  test 0.889
  rbf      train 0.810  test 1.000

Four things in that table, and all four belong in the journal.

Our own SVM is right. scikit-learn's linear SVC with C = 1 gave w = [2.1774, 1.2564] and b = -2.0961. Our own program, written from the hinge loss with plain sub-gradient steps, gave w = [2.2230, 1.1924] and b = -2.0817. Two independent programs agreeing to two decimal places on all three numbers is the strongest evidence available that the implementation is correct.

C = 0.1 is too small for every kernel. 0.524 training accuracy, which is worse than answering the commoner class. Under-penalised, the model gives up.

The number of support vectors falls as C rises. Linear: 20 at C = 0.1, 18 at C = 1, 13 at C = 10. A large C forces the model to take the training points seriously, so fewer of them end up inside the corridor.

The poly kernel scored 1.000 on the test set, and that is not a result. Nine test rows. One row is 0.111 of the accuracy, so 1.000 and 0.889 are one student apart. On a test set this small, rbf at 0.889 and poly at 1.000 have not been distinguished, and a write-up that declares poly the winner has read noise as a finding. Say the size of the test set beside every score.

munotes.in65

Practical 5: Support Vector Machines

Scaling. The unscaled runs came out at 0.810 and 0.889, which is no worse than the scaled ones, so on this dataset scaling did not decide anything. Scale anyway, and here is the honest reason: C and the RBF's gamma are defined in terms of distances, so their meaning changes when the units change. A C of 1 on features running to 98 is not the C of 1 in the table above, and a model tuned on one scale cannot be reused on another.

Procedure

  1. Write the dataset, scale both features to the range 0 to 1, and split 70 to 30 with a seeded shuffle.
  2. Implement the sub-gradient training loop: for each row compute y * (w.x + b); if it is below 1, apply both terms of the update; if not, apply only the shrink. Reduce the step size as the epoch number rises.
  3. Train for several values of C and record w, b and both accuracies for each.
  4. List the training points with the smallest margins: those are the support vectors.
  5. Plot the points, the boundary and the two margin lines.
  6. Run SVC with the linear, rbf and poly kernels at three values of C, and record the number of support vectors each keeps.
  7. Compare scikit-learn's w and b with your own.
  8. Run once without scaling and say what changed.

Observations

Our own SVM, 21 training and 9 test rowsw1w2btraintest
C = 0.010.02600.0053-0.12880.5710.444
C = 0.10.27910.1090-1.09310.5710.444
C = 12.22301.1924-2.08170.9050.667
C = 103.08842.3295-3.08840.9050.667
C = 1003.22742.4287-3.19660.9050.667
scikit-learn SVCtraintestsupport vectors
linear, C = 0.10.5240.55620
linear, C = 10.8100.88918
linear, C = 100.8100.88913
rbf, C = 10.8570.88915
rbf, C = 100.9050.88914
poly, C = 10.8571.00012
poly, C = 100.8101.00011
Cross-checkw1w2b
our program, C = 12.22301.1924-2.0817
scikit-learn linear SVC, C = 12.17741.2564-2.0961

Result

A linear soft-margin Support Vector Machine was implemented from the hinge loss and trained by sub-gradient descent on the thirty-student dataset, scaled and split 21 to 9. C was tuned over five values: below 1 the model failed, reaching 0.571 training accuracy, and from 1 upward it reached 0.905 training and 0.667 test. Its weights, [2.2230, 1.1924] with bias -2.0817, agree to two decimal places with scikit-learn's linear SVC. The five smallest margins identified the support vectors, of which the two smallest were the dataset's two deliberate exceptions, both accepted as errors by the soft margin. Across three kernels and three values of C, test accuracy on nine rows ranged from 0.556 to 1.000, a spread of four students, and the number of support vectors fell as C rose.

munotes.in66

Practical 5: Support Vector Machines

Where marks are lost

Not scaling, and not saying why. C and gamma are distances. Scale, or justify not scaling.

Reporting one value of C. MU says "optimize its parameters". A single run has optimised nothing; show the table.

Calling 1.000 on nine rows a result. Quote the size of the test set beside the score.

Labelling the classes 0 and 1. The margin y * (w.x + b) needs y to be +1 and -1. With 0 and 1 every margin for the zero class is zero and the model never learns.

Never naming the support vectors. They are the point of the model. List them.

A constant learning rate. The steps must shrink or the weights oscillate around the answer.

Saying an SVM "finds the best line" with no definition of best. Best means the widest corridor, subject to the price C on violations.

For the journal

Aim; the line, the labels +1 and -1, the margin and the hinge loss written out; the training program; the table of C against weights and accuracy; the support vectors listed with their margins; the plot with the boundary and both margin lines; the scikit-learn table across kernels and C; the comparison of your weights with scikit-learn's; both observation tables; the result; one paragraph analysing which setting you would ship and why.

Quick revision

  • An SVM finds the boundary with the widest empty corridor between the classes.
  • Labels are +1 and -1. The margin of a point is y * (w.x + b).
  • Support vectors are the points closest to the boundary. Only they decide it.
  • The soft margin allows violations and charges C for each.
  • Small C: wide corridor, many mistakes. Large C: narrow corridor, few mistakes, and a risk of overfitting.
  • The objective is (1/2)|w|^2 + C * sum of hinge losses.
  • Hinge loss is max(0, 1 - y*(w.x + b)): zero outside the corridor.
  • Kernels: linear, rbf (curved, the default), poly.
  • The kernel trick draws a curve without computing one, by changing how similarity is measured.
  • Scale the features: C and gamma are measured in the units of the data.
  • Fewer support vectors at a larger C.

Questions you must be able to answer

1. What does an SVM maximise? The margin: the width of the empty corridor between the two classes, measured to the nearest points on each side.

munotes.in67

Practical 5: Support Vector Machines

2. What is a support vector? A training point on or inside the margin. It is one of the few points that decide where the boundary goes; moving a point far from the boundary changes nothing.

3. What does C do? It sets the price of letting a training point violate the margin. Small C prefers a wide corridor and tolerates errors; large C insists on classifying the training points and narrows the corridor.

4. What happened at C = 0.01 in your run? The model reached 0.571 on the training rows and 0.444 on the test rows: the penalty was too small to make it separate anything.

5. What is the hinge loss? max(0, 1 - y*(w.x + b)). It is zero for a point at least one unit outside the boundary on the right side, and rises linearly as the point moves inside the corridor and beyond it.

6. Why must the labels be +1 and -1? Because the margin is the label times the signed distance, and that expression only tells the two sides apart when the labels have opposite signs.

7. What is the kernel trick, in one sentence? Replacing the ordinary dot product with another similarity function, so that a straight boundary in the new space of similarities becomes a curved boundary in the original one, without ever constructing that space.

8. Two of your support vectors had negative margins. Is the model broken? No. A negative margin means the point is on the wrong side, and the soft margin is what allows that. Both were the dataset's deliberate exceptions, and accepting them bought a wider corridor for everyone else.

9. Your poly kernel scored 1.000 on the test set. Is it the best model? Not on this evidence. The test set has nine rows, so 1.000 is one student away from 0.889, and several settings are within that. Report the figure with the size of the test set and say the difference is not measurable here.

10. Does the SVM need scaled features? It works without them here, but C and the RBF gamma are defined in terms of distances, so their meaning changes with the units. Scale, so that a tuned value means the same thing on the next dataset.

munotes.in68

The rest of this subject

These notes are cut from the University's printed syllabus. Open the syllabus itself for the same subject.

Issue
Done!