The C parameter in soft-margin SVMs, and how to choose it honestly
What the cost parameter C trades off in a soft-margin SVM, and why choosing it on the test set and then reporting training error gives a number you can't trust.
University lab From my coursework in the MSc in Statistics and Machine Learning at Linköping University, rewritten as a note to review later.
In a spam-classification lab I tuned an SVM, proudly reported 2.87% error, and called it “a good performance”. Looking back, that number meant almost nothing, and working out why taught me more than the SVM itself. First the idea. A support vector machine looks for the boundary with the widest gap between classes. But real data overlap, so a clean gap usually doesn’t exist. The SVM needs a knob that says how much to care about points on the wrong side. That knob is .
The soft-margin problem
For training points with labels , the soft-margin SVM solves
- define the boundary ; is the feature map behind the kernel (an RBF kernel in my lab).
- The margin width is , so a small means a wide margin.
- is the slack of point : 0 if it’s on the right side outside the margin, between 0 and 1 if it’s inside the margin, above 1 if it’s misclassified.
- is the price of each unit of slack.
So trades a wide margin (first term) against margin violations (second term). Equivalently, it’s an inverse regularization strength: think of as the penalty and as the loss (the hinge loss), and plays the role of .
- Small : violations are cheap. Wide margin, many support vectors, smoother boundary, more bias.
- Large : violations are expensive. The SVM bends the boundary to classify training points correctly. Narrow margin, fewer support vectors, more variance.
In the dual problem, shows up as a box constraint on the Lagrange multipliers, . A point with is a support vector, and caps how much any single point can pull on the boundary.
What I got wrong in the description
In the report I wrote that increasing lets us “penalize the weights” to reduce training error. It’s the other way round. Increasing penalizes the slacks more, so relative to that the penalty on becomes weaker. The weights are allowed to grow, the margin shrinks, and training error goes down. That’s why training error kept falling as grew while test error didn’t follow.
Choosing honestly
Here’s what I did in the lab: tried from 0.05 up to about 100, picked the value with the lowest test error (), refit on all data, and reported the training error of that final model (2.87%) as “a good performance”. Two problems:
- The test set was used for model selection. Once you pick because it did best on the test set, that test error is optimistically biased. You’ve tuned to it.
- Training error on the data the model was fit on is also optimistic, especially for a flexible RBF SVM, which can fit training data very closely.
So 2.87% isn’t an estimate of how the model will do on new emails. The fix is the usual three-way discipline: choose on a validation set (or by cross-validation on the training part), refit on train + validation, and look at the test set exactly once at the end. (If the rows come in groups, the split has to respect that too; see grouped cross-validation.)
library(e1071)
set.seed(3)
n <- 600
X <- matrix(rnorm(2 * n), ncol = 2)
y <- factor(ifelse(rowSums(X^2) + rnorm(n, sd = 0.7) > 1.4, "out", "in"))
d <- data.frame(X, y)
split <- sample(rep(c("train", "valid", "test"), c(300, 150, 150)))
tr <- d[split == "train", ]; va <- d[split == "valid", ]; te <- d[split == "test", ]
err <- function(model, data) mean(predict(model, data) != data$y)
Cs <- c(0.01, 0.1, 1, 10, 100, 1000)
res <- t(sapply(Cs, function(C) {
m <- svm(y ~ ., data = tr, kernel = "radial", gamma = 2, cost = C)
c(C = C, train = err(m, tr), valid = err(m, va), n_sv = m$tot.nSV)
}))
round(res, 3) # train error keeps falling with C; validation error is lowest at C = 1
best <- res[which.min(res[, "valid"]), "C"]
final <- svm(y ~ ., data = rbind(tr, va), kernel = "radial", gamma = 2, cost = best)
err(final, te) # about 0.15: the one number to report
In this run the training error falls from 0.47 to 0.10 as goes from 0.01 to 1000, the number of support vectors drops from 285 to 119, and the validation error is lowest at (0.16) and is higher (about 0.19 to 0.21) for every larger .
Two more things worth remembering. and the kernel width (sigma in kernlab, gamma in e1071) interact, so tune them together on a grid. And scale the features first; the RBF kernel uses Euclidean distance, so a feature measured in large units would dominate. (e1071::svm does this for you by default, scale = TRUE.)
Key takeaways
- is the cost of margin violations; large means narrow margin and low training error, small means wide margin and more bias.
- Large penalizes slack, not weights: it’s weak regularization, not strong.
- In the dual, is the upper bound on each .
- Pick on validation data or by cross-validation, never on the test set, and never report training error as performance.
Resources
- 732A99 Machine Learning, Linköping University (course lab, autumn 2019)
- Corinna Cortes and Vladimir Vapnik, “Support-vector networks”, Machine Learning 20 (1995): https://doi.org/10.1007/BF00994018
- Trevor Hastie, Robert Tibshirani, Jerome Friedman, The Elements of Statistical Learning, 2nd ed., Section 7.2 (train/validation/test) and Sections 12.2–12.3 (support vector classifiers and machines): https://hastie.su.domains/ElemStatLearn/
- Christopher M. Bishop, Pattern Recognition and Machine Learning, Section 7.1.1
- R package
e1071documentation (svm): https://cran.r-project.org/package=e1071