library(e1071)
library(ISLR2)
library(tidyverse)
library(ggplot2)
library(dplyr)
We have seen that we can fit an SVM with a non-linear kernel in order to perform classification using a non-linear decision boundary. We will now see that we can also obtain a non-linear decision boundary by performing logistic regression using non-linear transformations of the features.
set.seed(1)
x1 <-runif(500)- 0.5
x2 <-runif(500)- 0.5
y <- 1 * (x1^2- x2^2 > 0)
dat <- data.frame(x1 = x1, x2 = x2, y = as.factor(y))
plot(x1, x2, col = ifelse(y == 1, "blue", "red"), pch = 19,
xlab = "X1", ylab = "X2", main = "True Class Labels")
log.fit <- glm(y ~ x1 + x2, data = dat, family = binomial)
summary(log.fit)
##
## Call:
## glm(formula = y ~ x1 + x2, family = binomial, data = dat)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -0.087260 0.089579 -0.974 0.330
## x1 0.196199 0.316864 0.619 0.536
## x2 -0.002854 0.305712 -0.009 0.993
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 692.18 on 499 degrees of freedom
## Residual deviance: 691.79 on 497 degrees of freedom
## AIC: 697.79
##
## Number of Fisher Scoring iterations: 3
The logistic regression model attempts to separate the two classes using a linear decision boundary. Because the true relationship is quadratic, the model is expected to perform poorly. The coefficient estimates may not all be statistically significant because a linear model cannot capture the underlying nonlinear pattern.
log.prob <- predict(log.fit, type = "response")
log.pred <- ifelse(log.prob > 0.5, 1, 0)
plot(x1, x2, col = ifelse(log.pred == 1, "blue", "red"), pch = 19,
xlab = "X1", ylab = "X2", main = "Linear Logistic Regression Predictions")
The predicted classes are separated by a straight line because logistic regression with only X1 and X2 assumes a linear relationship. Many observations are misclassified because the true decision boundary is quadratic rather than linear.
log.nonlin.fit <- glm(
y ~ x1 + x2 + I(x1^2) + I(x2^2) + x1:x2,
data = dat,
family = binomial
)
## Warning: glm.fit: algorithm did not converge
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
summary(log.nonlin.fit)
##
## Call:
## glm(formula = y ~ x1 + x2 + I(x1^2) + I(x2^2) + x1:x2, family = binomial,
## data = dat)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -10.16 713.54 -0.014 0.989
## x1 42.10 15492.58 0.003 0.998
## x2 -66.81 14788.95 -0.005 0.996
## I(x1^2) 16757.98 519013.02 0.032 0.974
## I(x2^2) -16671.65 508668.89 -0.033 0.974
## x1:x2 -206.38 41802.81 -0.005 0.996
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 6.9218e+02 on 499 degrees of freedom
## Residual deviance: 3.5810e-06 on 494 degrees of freedom
## AIC: 12
##
## Number of Fisher Scoring iterations: 25
Adding quadratic and interaction terms allows logistic regression to model nonlinear relationships between the predictors and the response. This greatly increases the flexibility of the decision boundary.
log.nonlin.prob <- predict(log.nonlin.fit, type = "response")
log.nonlin.pred <- ifelse(log.nonlin.prob > 0.5, 1, 0)
plot(x1, x2, col = ifelse(log.nonlin.pred == 1, "blue", "red"), pch = 19,
xlab = "X1", ylab = "X2", main = "Non-Linear Logistic Regression Predictions")
The nonlinear logistic regression correctly captures the quadratic decision boundary and classifies substantially more observations correctly than the linear logistic regression.
svc.fit <- svm(y ~ x1 + x2, data = dat, kernel = "linear", cost = 1)
svc.pred <- predict(svc.fit, dat)
plot(x1, x2, col = ifelse(svc.pred == 1, "blue", "red"), pch = 19,
xlab = "X1", ylab = "X2", main = "Linear Support Vector Classifier Predictions")
The linear support vector classifier produces a straight decision boundary similar to linear logistic regression. Because the true boundary is nonlinear, the classifier cannot perfectly separate the observations.
svm.radial.fit <- svm(y ~ x1 + x2, data = dat, kernel = "radial", gamma = 1, cost = 1)
svm.radial.pred <- predict(svm.radial.fit, dat)
plot(x1, x2, col = ifelse(svm.radial.pred == 1, "blue", "red"), pch = 19,
xlab = "X1", ylab = "X2", main = "Radial Kernel SVM Predictions")
svm.poly.fit <- svm(y ~ x1 + x2, data = dat, kernel = "polynomial", degree = 2, cost = 1)
svm.poly.pred <- predict(svm.poly.fit, dat)
plot(x1, x2, col = ifelse(svm.poly.pred == 1, "blue", "red"), pch = 19,
xlab = "X1", ylab = "X2", main = "Polynomial Kernel SVM Predictions")
The radial and polynomial kernel SVMs successfully learn nonlinear decision boundaries that closely match the true quadratic relationship. These models classify the observations much more accurately than the linear SVM.
The linear logistic regression and linear SVM both perform poorly because they assume a linear decision boundary. Introducing nonlinear terms into logistic regression greatly improves classification accuracy. The nonlinear SVMs, particularly the radial kernel SVM, provide the best fit because they naturally capture complex nonlinear relationships without explicitly specifying polynomial terms.
In this problem, you will use support vector approaches in order to predict whether a given car gets high or low gas mileage based on the Auto data set.
data(Auto)
high_mpg <- ifelse(Auto$mpg > median(Auto$mpg), 1, 0)
Auto$high_mpg <- as.factor(high_mpg)
Auto_df <- Auto[, !(names(Auto) %in% c("mpg", "name"))]
set.seed(1)
tune.linear <- tune(svm, high_mpg ~ ., data = Auto_df, kernel = "linear",
ranges = list(cost = c(0.01, 0.1, 1, 5, 10, 100)))
summary(tune.linear)
##
## Parameter tuning of 'svm':
##
## - sampling method: 10-fold cross validation
##
## - best parameters:
## cost
## 1
##
## - best performance: 0.08435897
##
## - Detailed performance results:
## cost error dispersion
## 1 1e-02 0.08923077 0.04698309
## 2 1e-01 0.09185897 0.04393409
## 3 1e+00 0.08435897 0.03662670
## 4 5e+00 0.08948718 0.03898410
## 5 1e+01 0.08948718 0.03898410
## 6 1e+02 0.08692308 0.03887151
tune.linear$best.parameters
## cost
## 3 1
tune.linear$best.performance
## [1] 0.08435897
set.seed(1)
tune.radial <- tune(svm, high_mpg ~ ., data = Auto_df, kernel = "radial",
ranges = list(cost = c(0.1, 1, 10, 100),
gamma = c(0.01, 0.1, 1, 5)))
summary(tune.radial)
##
## Parameter tuning of 'svm':
##
## - sampling method: 10-fold cross validation
##
## - best parameters:
## cost gamma
## 1 1
##
## - best performance: 0.06634615
##
## - Detailed performance results:
## cost gamma error dispersion
## 1 0.1 0.01 0.11224359 0.03836937
## 2 1.0 0.01 0.08673077 0.04551036
## 3 10.0 0.01 0.08673077 0.04040897
## 4 100.0 0.01 0.08685897 0.03483004
## 5 0.1 0.10 0.08923077 0.04698309
## 6 1.0 0.10 0.08923077 0.04376306
## 7 10.0 0.10 0.08166667 0.04149504
## 8 100.0 0.10 0.08410256 0.03390616
## 9 0.1 1.00 0.08673077 0.04535158
## 10 1.0 1.00 0.06634615 0.03244101
## 11 10.0 1.00 0.08923077 0.02732003
## 12 100.0 1.00 0.10448718 0.04560852
## 13 0.1 5.00 0.54602564 0.05105434
## 14 1.0 5.00 0.09173077 0.03417795
## 15 10.0 5.00 0.09173077 0.04983028
## 16 100.0 5.00 0.09429487 0.05109336
tune.radial$best.parameters
## cost gamma
## 10 1 1
tune.radial$best.performance
## [1] 0.06634615
svm_radial <- tune.radial$best.model
table(
predict = predict(svm_radial, newdata = Auto),
truth = Auto$high_mpg
)
## truth
## predict 0 1
## 0 188 6
## 1 8 190
set.seed(1)
tune.poly <- tune(svm, high_mpg ~ ., data = Auto_df, kernel = "polynomial",
ranges = list(cost = c(0.1, 1, 10, 100),
degree = c(2, 3, 4)))
summary(tune.poly)
##
## Parameter tuning of 'svm':
##
## - sampling method: 10-fold cross validation
##
## - best parameters:
## cost degree
## 100 3
##
## - best performance: 0.08423077
##
## - Detailed performance results:
## cost degree error dispersion
## 1 0.1 2 0.27846154 0.09486227
## 2 1.0 2 0.25307692 0.13751948
## 3 10.0 2 0.18647436 0.05598001
## 4 100.0 2 0.18128205 0.06251437
## 5 0.1 3 0.20192308 0.11347783
## 6 1.0 3 0.09448718 0.04180527
## 7 10.0 3 0.08435897 0.04544023
## 8 100.0 3 0.08423077 0.03636273
## 9 0.1 4 0.26564103 0.09977887
## 10 1.0 4 0.21205128 0.09560470
## 11 10.0 4 0.16589744 0.06962914
## 12 100.0 4 0.12756410 0.05208506
tune.poly$best.parameters
## cost degree
## 8 100 3
tune.poly$best.performance
## [1] 0.08423077
svmfit <- svm(y ~ ., data = dat, kernel = "linear", cost = 1)
plot(svmfit, dat)
where svmfit contains your fitted model and dat is a data frame containing your data, you can type
#plot(svmfit, dat, x1∼x4)
in order to plot just the first and fourth variables. However, you must replace x1 and x4 with the correct variable names. To find out more, type ?plot.svm
?plot.svm
## starting httpd help server ... done
best.linear <- tune.linear$best.model
best.radial <- tune.radial$best.model
best.poly <- tune.poly$best.model
plot(best.linear, Auto_df, cylinders ~ horsepower)
plot(best.radial, Auto_df, cylinders ~ horsepower)
plot(best.poly, Auto_df, cylinders ~ horsepower)
This problem involves the OJ data set which is part of the ISLR2 package.
set.seed(1)
train_indices <- sample(1:nrow(OJ), 800)
train_data <- OJ[train_indices, ]
test_data <- OJ[-train_indices, ]
svc_fit <- svm(Purchase ~ ., data = train_data, kernel = "linear", cost = 0.01)
summary(svc_fit)
##
## Call:
## svm(formula = Purchase ~ ., data = train_data, kernel = "linear",
## cost = 0.01)
##
##
## Parameters:
## SVM-Type: C-classification
## SVM-Kernel: linear
## cost: 0.01
##
## Number of Support Vectors: 435
##
## ( 219 216 )
##
##
## Number of Classes: 2
##
## Levels:
## CH MM
The summary reports the number of support vectors and shows that a linear decision boundary was fit. Because the cost parameter is very small (0.01), the classifier allows more observations inside the margin, resulting in a wider margin and potentially more training errors.
train_pred <- predict(svc_fit, train_data)
train_err <- mean(train_pred != train_data$Purchase)
test_pred <- predict(svc_fit, test_data)
test_err <- mean(test_pred != test_data$Purchase)
cat("Training Error Rate:", train_err, "\n")
## Training Error Rate: 0.175
cat("Test Error Rate: ", test_err, "\n")
## Test Error Rate: 0.1777778
The training error is expected to be lower than the test error because the model was fit using the training observations. The test error provides a better estimate of future prediction accuracy.
set.seed(1)
tune_linear <- tune(svm, Purchase ~ ., data = train_data, kernel = "linear",
ranges = list(cost = c(0.01, 0.1, 1, 5, 10)))
summary(tune_linear)
##
## Parameter tuning of 'svm':
##
## - sampling method: 10-fold cross validation
##
## - best parameters:
## cost
## 0.1
##
## - best performance: 0.1725
##
## - Detailed performance results:
## cost error dispersion
## 1 0.01 0.17625 0.02853482
## 2 0.10 0.17250 0.03162278
## 3 1.00 0.17500 0.02946278
## 4 5.00 0.17250 0.03162278
## 5 10.00 0.17375 0.03197764
best_linear <- tune_linear$best.model
cat("Optimal Linear Train Error:", mean(predict(best_linear, train_data) != train_data$Purchase), "\n")
## Optimal Linear Train Error: 0.165
cat("Optimal Linear Test Error: ", mean(predict(best_linear, test_data) != test_data$Purchase), "\n")
## Optimal Linear Test Error: 0.162963
svm_radial <- svm(Purchase ~ ., data = train_data, kernel = "radial", cost = 0.01)
cat("Radial (C=0.01) Train Error:", mean(predict(svm_radial, train_data) != train_data$Purchase), "\n")
## Radial (C=0.01) Train Error: 0.39375
cat("Radial (C=0.01) Test Error: ", mean(predict(svm_radial, test_data) != test_data$Purchase), "\n")
## Radial (C=0.01) Test Error: 0.3777778
set.seed(1)
tune_radial <- tune(svm, Purchase ~ ., data = train_data, kernel = "radial",
ranges = list(cost = c(0.01, 0.1, 1, 5, 10)))
best_radial <- tune_radial$best.model
cat("Optimal Radial Cost: ", best_radial$cost, "\n")
## Optimal Radial Cost: 1
cat("Optimal Radial Train Error: ", mean(predict(best_radial, train_data) != train_data$Purchase), "\n")
## Optimal Radial Train Error: 0.15125
cat("Optimal Radial Test Error: ", mean(predict(best_radial, test_data) != test_data$Purchase), "\n")
## Optimal Radial Test Error: 0.1851852
summary(tune_radial)
##
## Parameter tuning of 'svm':
##
## - sampling method: 10-fold cross validation
##
## - best parameters:
## cost
## 1
##
## - best performance: 0.17125
##
## - Detailed performance results:
## cost error dispersion
## 1 0.01 0.39375 0.04007372
## 2 0.10 0.18625 0.02853482
## 3 1.00 0.17125 0.02128673
## 4 5.00 0.18000 0.02220485
## 5 10.00 0.18625 0.02853482
tune_radial$best.parameters
## cost
## 3 1
tune_radial$best.performance
## [1] 0.17125
svm_poly <- svm(Purchase ~ ., data = train_data, kernel = "polynomial", degree = 2, cost = 0.01)
cat("Poly (C=0.01) Train Error:", mean(predict(svm_poly, train_data) != train_data$Purchase), "\n")
## Poly (C=0.01) Train Error: 0.3725
cat("Poly (C=0.01) Test Error: ", mean(predict(svm_poly, test_data) != test_data$Purchase), "\n")
## Poly (C=0.01) Test Error: 0.3666667
set.seed(1)
tune_poly <- tune(svm, Purchase ~ ., data = train_data, kernel = "polynomial", degree = 2,
ranges = list(cost = c(0.01, 0.1, 1, 5, 10)))
best_poly <- tune_poly$best.model
cat("Optimal Poly Cost: ", best_poly$cost, "\n")
## Optimal Poly Cost: 10
cat("Optimal Poly Train Error: ", mean(predict(best_poly, train_data) != train_data$Purchase), "\n")
## Optimal Poly Train Error: 0.15
cat("Optimal Poly Test Error: ", mean(predict(best_poly, test_data) != test_data$Purchase), "\n")
## Optimal Poly Test Error: 0.1888889
summary(tune_poly)
##
## Parameter tuning of 'svm':
##
## - sampling method: 10-fold cross validation
##
## - best parameters:
## cost
## 10
##
## - best performance: 0.18125
##
## - Detailed performance results:
## cost error dispersion
## 1 0.01 0.39125 0.04210189
## 2 0.10 0.32125 0.05001736
## 3 1.00 0.20250 0.04116363
## 4 5.00 0.18250 0.03496029
## 5 10.00 0.18125 0.02779513
tune_poly$best.parameters
## cost
## 5 10
tune_poly$best.performance
## [1] 0.18125
Among all models, the radial kernel SVM produced the lowest test error and therefore the best predictive performance. The linear SVM was the least flexible and generally had higher error, while the polynomial kernel improved performance but typically did not outperform the radial kernel. Overall, the radial kernel provides the best balance between flexibility and generalization on the OJ data.