Загружаем нужные пакеты:
library("knitr")
# opts_chunk$set(cache=FALSE)
library("ggplot2") # графики
library("kernlab") # Support Vector Machines
library("dplyr") # манипуляции с данными
library("caret") # построение моделей
Другой популярный пакет для SVM в R — это e1071
.
Ядерная функция, или просто ядро, — это скалярное произведение в преобразованном пространстве.
\[ K(x,x')=(\phi(x),\phi(x')) \]
Гауссовское ядро: \[ K(x, x') = \exp(-\sigma |x-x'|^2) \]
x <- 1:6
y <- 1:6
k1 <- vanilladot()
k2 <- rbfdot(sigma = 1)
k1(x, y)
## [,1]
## [1,] 91
k2(x, y)
## [,1]
## [1,] 1
Скалярное произведение трудно интерпретировать “в лоб”. Лучшая известная мне интерпретация: скалярное произведение — это произведение длин векторов на косинус угла между ними, \((y,y')=|y|\cdot |y'| \cos(y,y')\).
Скалярное произведение содержит в себе всю информацию о геометрии пространства. Если знать чему равно скалярное произведение между любыми векторами, то можно посчитать длину любого вектора и угол между любыми векторами.
\[ \min_{w, w_0} \frac{1}{2} w'w + C \sum_{i=1}^n \xi_i \]
Уравнение разделяющей поверхности: \(w'x = w_0\), края полосы: \(w'x=w_0+1\) и \(w'x=w_0-1\). Нарушителями считаются наблюдения, которые попали на нейтральную полосу или на чужую территорию. Здесь \(\xi_i = |w| \cdot d_i\), где \(d_i\) — “заступ” наблюдения за черту “своих”.
SVM пытается разделить гиперплоскостью наши данные в более многомерном пространстве, чем исходное, видео. Это новое пространство называется спрямляющим.
Для гауссовского ядра это спрямляющее пространство будет бесконечномерным, а все векторы превращаются в векторы единичной длины.
Загружаем данные по стоимости квартир в Москве:
filename <- "../datasets/flats_moscow.txt"
flats <- read.table(filename, header = TRUE)
str(flats)
## 'data.frame': 2040 obs. of 11 variables:
## $ n : int 1 2 3 4 5 6 7 8 9 10 ...
## $ price : int 81 75 128 95 330 137 98 88 225 140 ...
## $ totsp : int 58 44 70 61 104 76 59 55 80 86 ...
## $ livesp : int 40 28 42 37 60 50 39 36 56 51 ...
## $ kitsp : num 6 6 6 6 11 9 6 6 9 10 ...
## $ dist : num 12.5 13.5 14.5 13.5 10.5 11 7.5 9 9 12.7 ...
## $ metrdist: int 7 7 3 7 7 7 10 5 5 10 ...
## $ walk : int 1 1 1 1 0 1 0 1 1 1 ...
## $ brick : int 1 0 1 0 1 1 0 1 1 0 ...
## $ floor : int 1 1 1 1 1 1 1 0 1 1 ...
## $ code : int 3 6 3 1 3 8 8 4 3 5 ...
Для ksvm нужно указать, что зависимая переменная — факторная, а не количественная. Более того, названия категорий должны быть валидными именами для переменных.
flats$brick <- as.factor(flats$brick)
levels(flats$brick) <- list(no = "0", yes = "1")
flats$walk <- as.factor(flats$walk)
Разделим выборку на две части — 75% для обучения и 25% для оценки качества обучения. Указание зависимой переменной важно, т.к. R случайно отберет 75% единичек и 75% нулей в обучающую выборку.
set.seed(777)
train_index <- createDataPartition(y = flats$brick, p = 0.75, list = FALSE)
train_flats <- flats[train_index, ]
test_flats <- flats[-train_index, ]
Команда set.seed
обеспечивает воспроизводимость эксперимента на другом компьютере.
Находим оптимальную разделяющую гиперплоскость:
m1 <- ksvm(brick ~ price + totsp + livesp + kitsp + dist + metrdist + walk + floor + code,
data = train_flats,
kernel = "rbfdot",
kpar = list(sigma = 0.05),
C = 5)
Настраивать можно:
Тип ядра, kernel = "rbfdot"
Параметры ядра, если они у него есть, kpar = list(sigma = 0.05)
Величину штрафа за неправильно классифицированные объекты, C = 5
Есть встроенные методы подбора параметра \(\sigma\) для Гауссовского ядра:
m1 <- ksvm(brick ~ price + totsp + livesp + kitsp + dist + metrdist + walk + floor + code,
data = train_flats,
kernel = "rbfdot",
C = 5)
m1
## Support Vector Machine object of class "ksvm"
##
## SV type: C-svc (classification)
## parameter : cost C = 5
##
## Gaussian Radial Basis kernel function.
## Hyperparameter : sigma = 0.110127445667694
##
## Number of Support Vectors : 786
##
## Objective Function Value : -2946.599
## Training error : 0.157413
Слабо добраться до оптимального автоматически найденного \(\sigma\)?
kernelf(m1) # описание ядра: тип и параметры
## Gaussian Radial Basis kernel function.
## Hyperparameter : sigma = 0.110127445667694
kpar(kernelf(m1))$sigma # из описания извлекаем параметры, из них --- сигму
## [1] 0.1101274
Вектора, лежащие на границе разделяющей полосы, называются опорными.
Найденная оптимальная гиперплоскость не даёт нам простой интепретации зависимости. Из объекта m1
можно извлечь опорные вектора, но в реальной практической задаче их как минимум сотни, и что с ними делать в такой ситуации мне не известно.
Строим прогнозы для тестовой части выборки:
test_flats$brick.pred <- predict(m1, test_flats)
table(test_flats$brick.pred, test_flats$brick)
##
## no yes
## no 308 75
## yes 37 89
Также можно спрогнозировать расстояние до разделяющей гиперплоскости…
…
Скомбинировав SVM и logit, можно в каком-то смысл оценить \(P(y_i=1)\) с помощью SVM. Сначала нужно построить прогноз по SVM для всех объектов в выборке. (тут точнее…) Используя расстояние от каждого прогноза до разделяющей гиперплоскости мы оценим модель похожую на logit…
Этот подход уже автоматизирован:
m1 <- ksvm(brick ~ price + totsp + livesp + kitsp + dist + metrdist + walk + floor + code,
data = train_flats,
kernel = "rbfdot",
kpar = list(sigma = 0.05),
C = 5,
prob.model = TRUE)
probs <- predict(m1, test_flats, type = "probabilities")
head(probs)
## no yes
## [1,] 0.4157523 0.5842477
## [2,] 0.7558480 0.2441520
## [3,] 0.5577324 0.4422676
## [4,] 0.3340514 0.6659486
## [5,] 0.2358058 0.7641942
## [6,] 0.3530213 0.6469787
Используя этот подход можно построить и кривые предельных эффектов.
В случае, если SVM идеально разделяет игреки на обучающей выборке оценить вероятности не получится. Дело здесь в том, что при идеальной классификации не существует оптимума правдоподобия в задаче оценки логит-модели.
Красивые графики, как мне кажется, можно получить только для случая двух объясняющих переменных
Пример из документации:
x <- rbind(matrix(rnorm(120), ncol = 2), matrix(rnorm(120, mean = 3), ncol = 2))
y <- matrix(c(rep(1, 60), rep(-1, 60)))
svp <- ksvm(x, y, type = "C-svc")
plot(svp, data = x)
Закрашенные треугольники и кружочки — это опорные вектора, т.е. те точки, которые оказались на границе разделяющей полосы в спрямляющем пространстве.
Во многих моделях есть параметр, отвечающий за простоту модели. Чем проще модель, тем хуже модель описывает выборку, по которой она оценивалась.
В линейной регрессии параметр сложности модели — это количество регрессоров, \(k\). Чем больше регрессоров, тем ниже сумма квадратов остатков, \(RSS\).
На гистограмме параметр сложности — число столбцов.
В ridge regression и LASSO параметр простоты — это \(\lambda\). Действительно, LASSO минимизирует \[ \sum_{i=1}^n (y_i-\hat{y}_i)^2+\lambda \sum_{j=1}^k |\hat{\beta}_j| \] Значит, чем больше параметр \(\lambda\), тем больше стремление алгоритма LASSO занулить некоторые \(\hat{\beta}_j\).
В SVM за “простоту” отвечают \(\sigma\) и \(C\).
Этот параметр сложности не так легко оценить, как коэффициенты модели. Если наивно попытаться выбрать параметр сложности так, чтобы модель как можно лучше описывала бы выборку, по которой она оценивалась… То ничего хорошего не выйдет. Окажется, что оптимальное количество регрессоров равно плюс бесконечности, а оптимальное \(\lambda\) в LASSO равно нулю. В регрессии одно из решений этой проблемы — это проверка гипотез о значимости коэффициентов.
Есть и универсальный способ выбора сложности модели — кросс валидация (перекрёстная проверка, cross validation). Её идея состоит в том, что надо оценивать качество прогнозов не по той же выборке, на которой оценивалась модель, а на новых наблюдениях.
set.seed()
для воспроизводимости эксперимента.Популярные значения k:
Создаём табличку перебираемых \(C\) и \(\sigma\):
C <- c(1, 10, 100)
sigma <- c(0.1, 1, 10)
d <- expand.grid(C, sigma) # Случайно не декартово ли произведение это? :)
colnames(d) <- c("C", "sigma")
head(d)
## C sigma
## 1 1 0.1
## 2 10 0.1
## 3 100 0.1
## 4 1 1.0
## 5 10 1.0
## 6 100 1.0
Пробуем 10-кратную кросс-валидацию для конкретных \(C\) и \(\sigma\):
set.seed(33222233) # любимый seed Фрекен Бок
m1 <- ksvm(brick ~ price + totsp + livesp + kitsp + dist + metrdist + walk + floor + code,
data = flats,
kernel = rbfdot,
kpar = list(sigma = 1),
C = 1,
cross = 10)
cross(m1) # cross validation error
## [1] 0.2210784
Для удобства оформляем это в функцию двух переменных:
f_cross <- function(sig, C) {
model <- ksvm(brick ~ price + totsp + livesp + kitsp + dist + metrdist + walk + floor + code,
data = flats,
kernel = rbfdot,
kpar = list(sigma = sig),
C = C,
cross = 10)
return(cross(model))
}
f_cross(1, 1) # тестируем сделанную функцию
## [1] 0.2279412
Применяем её ко всем возможным \(C\) и \(\sigma\):
d <- mutate(d, cr = f_cross(sigma, C))
head(d)
## C sigma cr
## 1 1 0.1 0.475
## 2 10 0.1 0.475
## 3 100 0.1 0.475
## 4 1 1.0 0.475
## 5 10 1.0 0.475
## 6 100 1.0 0.475
Есть замечательный пакет caret
созвучный с carrot :) В нем реализовано и деление выборки на тестовую и обучающую части и подбор параметров с помощью кросс-валидации для кучи методов, в том числе для SVM. Морковка очень вкусная :)
library(caret)
ctrl <- trainControl(classProbs = TRUE)
fit <- train(brick ~ price + totsp + livesp + kitsp + dist + metrdist + walk + floor + code,
data = train_flats,
method = "svmRadial",
trControl = ctrl)
fit
## Support Vector Machines with Radial Basis Function Kernel
##
## 1531 samples
## 10 predictor
## 2 classes: 'no', 'yes'
##
## No pre-processing
## Resampling: Bootstrapped (25 reps)
## Summary of sample sizes: 1531, 1531, 1531, 1531, 1531, 1531, ...
## Resampling results across tuning parameters:
##
## C Accuracy Kappa
## 0.25 0.7680351 0.4340640
## 0.50 0.7733471 0.4488185
## 1.00 0.7748407 0.4539361
##
## Tuning parameter 'sigma' was held constant at a value of 0.09918708
## Accuracy was used to select the optimal model using the largest value.
## The final values used for the model were sigma = 0.09918708 and C = 1.
Можно прогнозировать с помощью наилучшей модели
head(predict(fit, test_flats , type = "prob"))
## no yes
## 1 0.3985826 0.6014174
## 2 0.7497911 0.2502089
## 3 0.4992112 0.5007888
## 4 0.2976260 0.7023740
## 5 0.2369349 0.7630651
## 6 0.3789946 0.6210054
Если \(y\) принимает больше двух значений, то можно использовать метод “все против всех”:
Естественно, это уже автоматизировано. Есть и другие методы.
Чайники используют следующую процедуру оценивания SVM:
Суровые челябинские жители советают делать так:
rbfdot
в пакете kernlab
.A Practical Guide to Support Vector Classiffication, отсюда взята инструкция по практике SVM
A User’s Guide to Support Vector Machines — картинки, помогающие понять разницу гауссовского ядра для разных \(C\) и \(\sigma\) с примерами на питоне. Питон — это ещё один подходящий язык програмирования для анализа данных.
Пакет caret: официальный сайт, статья в JSS, и введение побольше. Пакет достаточно активно обновляется и это радует :)
Воронцов, Лекции по методу опорных векторов