R筆記 – (14)Support Vector Machine/Regression(支持向量機SVM)

skydome20

2017/07/09

返回主目錄

【如果喜歡,你也可以免費支持 R 系列筆記!】


本篇目錄

  1. 簡言
  2. Support Vector Machine(SVM):如何用SVM來分類(類別)
  3. SVM在多元分類(multi-class)的技巧
  4. Support Vector Regression(SVR):如何用SVR來預測(連續)
  5. 參數討論
  6. 參數調整(Tune Parameters):如何調整SVM的參數,讓模型表現出最佳狀態
  7. 總結
  8. Reference

0. 簡言

今天要介紹的是Top 10 algorithms in data mining之中的SVM,也就是「支持向量機」,Support Vector Machine。

SVM是一種知名的二元分類器(binary classifier),由俄羅斯的統計學家Vapnik 等人所提出,一種基於統計學習理論的演算法。

簡單地說,SVM是一種監督式學習的演算法,試圖從資料中建構一個超平面(hyperplane),將資料區分成兩個類別(2 classes),最後進行預測/分類。

若沒有學過SVM的人,光看上面這句話一定一頭霧水。但我也不打算在此解釋SVM演算法的細節與延伸。有興趣的話,可以參考下面的連結自我學習:

(強烈建議先將SVM的演算法弄熟,再繼續往下閱讀)

最後,本篇筆記會使用台大林智仁教授所開發的libsvm,在R中對應的套件是e1071

require(e1071)

1. Support Vector Machine(SVM)

一直以來,SVM就是資料科學中最受歡迎的分類演算法之一。

無論是小數據的運用(和深度學習需要大數據Big Data在背後支援不同);非線性可分(non-linear separability)的問題;高維模式識別問題上(醫學、圖像辨識…),SVM都有不錯的表現。

其概念是建構一個超平面(hyperplane),讓資料在空間中能夠被區分成兩類,所以又被稱為二元分類器(binary classifier)。

在二維的空間中,超平面指的就是「一條線」;三維空間中,則是「一個平面」;之所以有一個「超」字,是因為資料往往不會只有二維、三維。在更高維的空間中,我們無法觀察這個平面的形狀為何,於是就用「超平面(hyperplane)」一詞來概括。

(二維空間中的SVM超平面)


(三維空間中的SVM超平面)

在這裡,我們使用套件mlbench中的資料Glass來簡單示範。

這是一個分類問題,資料中記錄了六種玻璃材質內的化學元素含量,共有214個觀測值,10個變數:

require("mlbench")
data(Glass, package="mlbench")
data = Glass

e1017的套件裡,我們會使用svm()這個函式來建模。

我們先將Glass資料分成80%的訓練資料(Train),20%的測試資料(Test):

smp.size = floor(0.8*nrow(data)) 
set.seed(516)                     
train.ind = sample(seq_len(nrow(data)), smp.size)
train = data[train.ind, ] # 80%
test = data[-train.ind, ] # 20%

接著使用svm()訓練SVM的分類模型:

model = svm(formula = Type ~ .,  # 依變數(在這裡是Type)的資料形態要是Factor
            data = train)

# 可以看到SVM預設的參數設定
summary(model)
## 
## Call:
## svm(formula = Type ~ ., data = train)
## 
## 
## Parameters:
##    SVM-Type:  C-classification 
##  SVM-Kernel:  radial 
##        cost:  1 
##       gamma:  0.1111111 
## 
## Number of Support Vectors:  146
## 
##  ( 48 56 9 13 12 8 )
## 
## 
## Number of Classes:  6 
## 
## Levels: 
##  1 2 3 5 6 7

接下來針對訓練資料、測試資料進行預測,使用predict()函式:

# 預測
train.pred = predict(model, train)
test.pred = predict(model, test)

# 訓練資料的混淆矩陣
table(real=train$Type, predict=train.pred)
##     predict
## real  1  2  3  5  6  7
##    1 44 11  0  0  0  0
##    2 12 51  0  0  0  0
##    3  5  8  0  0  0  0
##    5  0  1  0  9  0  0
##    6  0  1  0  0  7  0
##    7  1  0  0  0  0 21
# 訓練資料的分類準確率
confus.matrix = table(real=train$Type, predict=train.pred)
sum(diag(confus.matrix))/sum(confus.matrix)
## [1] 0.7719298
# 測試資料的混淆矩陣
table(real=test$Type, predict=test.pred)
##     predict
## real  1  2  3  5  6  7
##    1 13  2  0  0  0  0
##    2  1 12  0  0  0  0
##    3  4  0  0  0  0  0
##    5  0  1  0  2  0  0
##    6  1  0  0  0  0  0
##    7  0  1  0  0  0  6
# 測試資料的分類準確率
confus.matrix = table(real=test$Type, predict=test.pred)
sum(diag(confus.matrix))/sum(confus.matrix)
## [1] 0.7674419

在訓練及測試資料上都得到76%左右的準確率,效果還不錯,而且模型看起來並沒有發生overfitting的問題。

而且,這僅僅只是使用預設參數(default)所建立的SVM分類模型。

只要懂得進一步調整參數,便可以讓模型的表現更勝一層樓。(相關討論會在第4節進行)


2. SVM在多元分類(multi-class)的技巧

在第1節,我們很輕易地針對一個具有七種類別(7 classes)的資料,使用一個svm()的函式就建完模了。

不過…似乎有哪裡怪怪的?

如果你有這樣的疑問或感覺,表示你看到了某些關鍵點。

先回到一開始,不知道你還記不記得一件事:SVM,是一種「二元分類器(binary classifier)」!

換句話說,一個SVM的模型,只能處理一個具有兩種類別(2 classes)的資料。

然而,我們原本的資料卻有六種類別(6 classes)…話說到這理,你應該也看出問題在哪裡了吧:

「一個二元分類器SVM,怎麼解決有六個類別的多元分類(multi-class)問題呢?」

這個問題,便是這一小節要討論的!


在處理多元分類的問題時,最常用的是以下兩種解法/策略:

(1) One-against-Rest(One-vs-All, OvA, OvR):一對多

這個策略的想法是,就是針對每一個類別,分別建立一個SVM(或其他二元分類器):屬於此類別的樣本視為(+1),其他類別的樣本視為(-1),如此一來,就轉換成一個二元分類的問題了!

以第1節的資料為例,資料中有六個類別(1~6,玻璃材質),那我們便會建立六個SVM:

  • 第一個SVM:屬於類別1的資料為(+1),其他類別為(-1),這個SVM用來區別這兩者

  • 第二個SVM:屬於類別2的資料為(+1),其他類別為(-1),這個SVM用來區別這兩者

  • 第三個SVM:屬於類別3的資料為(+1),其他類別為(-1),這個SVM用來區別這兩者

  • 以此類推…

換句話說,針對有t個類別的資料,就會存在著t個SVM。

當有一筆新資料要預測時,會分別丟進這t個SVM,得到t組值(v1, v2, v3…vt),再從中判別最大的值(如果類別是+1,-1)出現第幾個位置,那這筆資料便是屬於那一類。

這樣的做法很直覺,而且執行時間與記憶體並不會消耗太多。

但缺點是,將「剩下類別」視為同一個類別(-1)的這種做法,很容易導致(+1,-1)之間的資料筆數差距很大,也就是類別不平衡(class imbalance)的問題。


(2) One-against-One(OvO):一對一

這個策略的想法,很像高中數學的排列組合:從T種類別中任取2種類別,共會有幾種組合?

答案是:C(T,2) = T(T-1)/2

所以在這個策略底下,我們便會從多元類別(multi classes)的資料中,任選某兩個類別(2 classes)的資料,訓練一個SVM(只能區分這兩個類別),並重複這樣的動作,直到所有的類別組合,都有其對應的SVM為止。

因此,最後會有 T(T-1)/2 個SVM模型。

當有一筆新資料要預測時,會分別丟進這 T(T-1)/2 個SVM,每一個SVM都會將這筆資料分到某一類,就像是投票一樣,該類別會記錄+1,最後判斷哪一個類別獲得最多票數,即可預測這筆資料屬於哪一個類別。

和One-against-Rest不同,這樣比較並不會造成類別不平衡的問題;

但相對的,這個策略所需的運行時間較長;也吃較多的記憶體;而且有時候會發生兩個以上的類別獲得同票數的狀況,造成判斷上的困擾。

另一個預測的思維是,用淘汰賽的方式(來源:Support Vector Machines 簡介(林宗勳)):


以上就是在面對多元分類問題時,最常用的兩種解法。

這兩種解法各有優劣,很難說哪一種方式比較好。在這篇paper中也提到,要使用哪一種方法,其實端看你的資料狀況而定。

然而,我們在第1節中,其實並沒有使用上面兩種方法,只是單純使用e1071套件裡的svm(),就針對六個類別的資料訓練出SVM。這又是怎麼回事呢?

這個問題的答案,提供在林智仁實驗室的LIBSVM官網FAQ中,裡面提到:他們是使用One-against-One來解決多元分類的問題。

所以說,在使用svm()的時候,裡面其實已經做了One-against-One。


3. Support Vector Regression(SVR)

SVR是本來SVM的延伸型態,能夠處理連續的預測問題。

e1071套件裡面,並沒有一個函式叫做svr(),而是一樣用svm()

差別只在於:

  • 依變數的型態是factor時,svm()會建立SVM的超平面,來處理分類問題

  • 依變數的型態是numeric,svm()會轉為SVR,進行連續值的預測。

這裡簡單手動建立一筆資料:

data = data.frame(x=1:20,
                  y=c(3,4,8,2,6,10,12,13,15,14,17,18,20,17,21,22,25,30,29,31))

# 資料的原始值
plot(data$x, data$y, pch=16, xlab="X", ylab="Y")

我們可以先拉一條簡單的線性迴歸:

model <- lm(y ~ x , data) 

# lm預測
lm.pred = predict(model, data)

# 資料的原始值(黑點)
plot(data$x, data$y, pch=16, xlab="X", ylab="Y")
# lm的預測值(紅三角形)
points(lm.pred, pch=2, col="red")
abline(model, col="red")

我們可以直接用SVR來建模、預測:

model <- svm(y ~ x , data) # 依變數的型態要是numeric

# 預測
svr.pred = predict(model, data)

# 資料的原始值(黑點)
plot(data$x, data$y, pch=16, xlab="X", ylab="Y")
# SVR的預測值(藍叉)
points(svr.pred, pch=4, col="blue")

最後比較一下線性迴歸和SVR的表現,同時計算RMSE(root mean square error):

# 資料的原始值(黑點)
plot(data$x, data$y, pch=16, xlab="X", ylab="Y")
# lm的預測值(紅三角形)
points(lm.pred, pch=2, col="red")
# SVR的預測值(藍叉)
points(svr.pred, pch=4, col="blue")

# (lm, SVR) in RMSE
c(sqrt(mean((data$y - lm.pred)^2)),
  sqrt(mean((data$y - svr.pred)^2))
)
## [1] 1.914203 1.795094

可以發現到,在這個例子,SVR比lm的效果還要好一些些(1.79 < 1.91)。


4. 參數討論

對我來說,這一小節是最重要的部分!

前面在做SVM、SVR,我們都是用預設的參數來建模,並且都得到還不錯的效果,這是因為我們的資料並不複雜。

但是現實中,這種事情幾乎不可能發生。所以我們必須了解SVM裡面的參數,了解它們的意義,了解如何有效率地調整他們,讓模型既不會發生overfitting,同時表現合乎預期。

(在開始前,強烈建議先將SVM的演算法細節搞懂,否則這一小節會看不懂。)

那麼,我們就開始吧!

在SVM,同時也是e1071套件裡的svm()裡面,有一些重要的參數值得關注,並會針對其中幾個(*)進一步討論:

svm(...
    type    = 決定svm是要用來classification(類別)、還是regression(連續)。
    scale   = 將資料正規化成(平均值, 標準差) = (0,1) 的分佈。
    kernel  = 將資料映射到特徵空間的kernel-fun,用來處理「非線性可分」的問題。
    
    *cost    = 在Lagrange formulation中的大C,決定給被誤差/分錯的資料「多少」懲罰值。
    
    *epsilon = margin of tolerance。越大,表示在容忍範圍內的誤差/分錯的資料,不會被懲罰;反之,越接近0,每一個誤差/分錯的資料都會被懲罰。
    
    *gamma   = 在kernel-fun裡面的參數(linear-fun除外)。
    ...
)

# 更多詳情請參考R的官方Help文件

先來談談C,也就是上面的cost吧!


C(cost)

一開始的SVM,是要尋找一個能夠完美將「所有」資料分成兩邊,具有最大margin的超平面,這又被稱為“hard-margin SVM”。

但由於hard-margin SVM,追求要將資料完美分好,因此很容易有overfitting的風險。於是1995年,Vapnik等人提出了“soft-margin SVM”,讓SVM能容許一些被分錯的資料存在。

在soft-margin SVM的損失函數(loss function)中,這個大C的存在,就是容錯項。

藉由C,我們能給予那些被分錯的資料懲罰值,控制support vectors(用來決定超平面的那些資料點)的影響力。

  • soft-margin SVM的 primal 型態

  • 轉為Lagrange Formulation


換句話說:

  • C越大,代表容錯越小,越少support vectors,越接近hard-margin SVM的概念,卻容易overfitting

  • C越小,代表容錯越大,越多support vectors,可以追求更大的margin

如下圖所示,圈圈的點代表support vectors,用來決定margin的大小。

當C=1000時,support vectors的點幾乎都發生在線上面,很接近hard-margin SVM的概念。

當C越來越小,隨著support vectors的點越來越多,表示margin的範圍越來越大。

以第1節的例子來說,我們依序提高C的值(從1~1000),然後觀察support vectors的數量變化:

require("mlbench")
data(Glass, package="mlbench")
data = Glass

# 記錄每一次提高C時,對應的support vectors數量
num.SV = sapply(X=1:1000, 
                FUN=function(C) svm(Type~., data, cost=C, epsilon =.1)$tot.nSV)

# 隨著C越大,support vectors數量越少,代表margin的範圍越窄,越接近hard-margin SVM,越容易overfitting
plot(x=1:1000, y=num.SV, xlab="C value", ylab="# of support vectors", pch=16, cex=.5, main="# of SVs in soft-margin SVM")


Epsilon (ε)

這個參數主要影響的會是SVR,而非SVM。因為在SVR的損失函數中,使用的是epsilon intensive hinge loss。

Epsilon(ε)的概念是給予一個margin of tolerance,創造一個「絕對領域」的感覺。

在「絕對領域」內的資料點會被忽視,它們的殘差(error)一點幫助也沒有,換句話說:它們對訓練SVR一點幫助也沒有。

如下圖所示,Epsilon(ε)會創造一個width of zone,讓在這個容忍區塊裡面的資料被忽視。


因此我們可以這樣說:

  • Epsilon越大,代表容忍區塊越大,越多資料會被忽視,造成模型的準確度越低,support vectors的數量減少(記得,所謂的support vectors是那些across margin的資料點,它們的殘差會被納入考量,用來決定最後的margin)。

  • Epsilon越低(→0+),所有的資料殘差(error)都會被考慮,卻也容易造成overfitting。


這邊改以第3節的SVR案例來說明,當逐漸調高Epsilon(0~1),觀察support vectors的數量:

df = data.frame(x=1:20,
                y=c(3,4,8,2,6,10,12,13,15,14,17,18,20,17,21,22,25,30,29,31))


# 記錄每一次提高Epsilon時,對應SVR中的support vectors數量
num.SV = sapply(X=seq(0,1,0.01), 
                FUN=function(e) svm(y~x, df, cost=1, epsilon =e)$tot.nSV)
# 隨著Epsilon越大,support vectors的數量減少
plot(x=seq(0,1,0.01), y=num.SV, xlab="ε value", ylab="# of support vectors", pch=16, cex=.5, main="# of SVs in SVR")

# 記錄每一次提高Epsilon時,對應SVR中的RMSE
RMSE = sapply(X=seq(0,1,0.01), 
              FUN=function(e) sqrt(mean((svm(y~x, df, cost=1, epsilon =e)$residuals)^2)))
# 隨著Epsilon越大,模型的準確度越低,RMSE提高
plot(x=seq(0,1,0.01), y=RMSE, xlab="ε value", ylab="RMSE", pch=16, cex=.5, main="RMSE in SVR")


Gamma

這是用在kernel function中的參數,主要是polynomial、radial basis(RBF)和sigmoid。

這邊有不同kernel function的優劣討論,可以事先讀一下。

Gamma的意義比較難以說明,當使用kernel function將原始資料映射到特徵空間(feature space)時,它隱含地決定了資料在特徵空間的分佈狀況。

以幾何的觀點來看,當gamma增加時,會讓Radial Basis Function(RBF)裡面的σ變小,而σ很小的高斯分佈會又高又瘦,讓只在附近的資料點有所作用。(參考)

在定義中,Gamma = How far the influence of a single training example reaches,意思是(參考):

  • gamma大,資料點的影響力範圍比較近,對超平面來說,近點的影響力權重較大,容易勾勒出擬合近點的超平面,也容易造成overfitting。

  • gamma小,資料點的影響力範圍比較遠,對超平面來說,較遠的資料點也有影響力,因此能勾勒出平滑、近似直線的超平面。

這裡拿第1小節的SVM來說明,當陸續提高gamma的值時,所對應的訓練資料、測試資料的分類準確率度:

require("mlbench")
data(Glass, package="mlbench")
data = Glass
smp.size = floor(0.8*nrow(data)) 
set.seed(516)                     
train.ind = sample(seq_len(nrow(data)), smp.size)
train = data[train.ind, ] # 80%
test = data[-train.ind, ] # 20%

# 記錄每一次提高gamma時,對應的訓練資料的分類準確率
train.accuracy = sapply(X=seq(0.1,10,0.1), 
                        FUN=function(g){
                        model = svm(Type~., train, gamma=g, epsilon =.1)
                        pred = predict(model, train)
                        confus.matrix = table(real=train$Type, predict=pred)
                        sum(diag(confus.matrix))/sum(confus.matrix)
                        } 
)

# 記錄每一次提高gamma時,對應的測試資料的分類準確率
test.accuracy = sapply(X=seq(0.1,10,0.1), 
                       FUN=function(g){
                       model = svm(Type~., train, gamma=g, epsilon =.1)
                       pred = predict(model, test)
                       confus.matrix = table(real=test$Type, predict=pred)
                       sum(diag(confus.matrix))/sum(confus.matrix)
                       } 
)


# Train accuracy(紅點)
plot(x=seq(0.1,10,0.1), y=train.accuracy, pch=16, cex=.5, col="red", ylim=c(0,1),xlab="gamma value", ylab="Class Accuracy", main="Accuracy in soft-margin SVM")
# Test accuracy(藍點)
points(x=seq(0.1,10,0.1), y=test.accuracy, pch=16, cex=.5, col="blue")

legend("bottomright", pch = 16, col = c("red","blue"),legend=c("Train-Accuracy", "Test-Accuracy"))

隨著gamma增加,訓練資料的準確度也提高,但測試資料的準確度卻降低,表示有overfitting的問題產生。

(更多SVM參數的討論可以參閱這篇:SVM Parameters)


5. 參數調整(Tune Parameters)

在調參數的階段,所使用的手法被稱為grid search,概念是針對每一種參數組合,都會訓練一個對應的模型,最後觀察模型的表現,挑出表現最佳的模型。

在訓練的過程中,R會自動引入cross validation的手法,確保模型的可靠度(Robustness),讓tune出來的參數是可以採用的。

e1071的套件裡,有內建tune()的函式可以讓我們來調參數。

Tune parameters in SVM(soft-margin)

在SVM中,一般會去調的參數是(cost, gamma):

# data
require("mlbench")
data(Glass, package="mlbench")
data = Glass

# tune cost and gamma in SVM(soft-margin)
tune.model = tune(svm,
                  Type~.,
                  data=data,
                  kernel="radial", # RBF kernel function
                  range=list(cost=10^(-1:2), gamma=c(.5,1,2))# 調參數的最主要一行
)
  • 在訓練的過程中,我們訓練的是一堆模型,包含cost=10^-1, 10^0, 10^1, 10^2,gamma=0.5, 1, 2,這兩種參數的排列組合,換句話說,會有 4x3=12 個SVM模型。

  • 裡面的值是classification error

summary(tune.model)
## 
## Parameter tuning of 'svm':
## 
## - sampling method: 10-fold cross validation 
## 
## - best parameters:
##  cost gamma
##    10   0.5
## 
## - best performance: 0.2937229 
## 
## - Detailed performance results:
##     cost gamma     error dispersion
## 1    0.1   0.5 0.6062771 0.10333568
## 2    1.0   0.5 0.3504329 0.07419505
## 3   10.0   0.5 0.2937229 0.06307295
## 4  100.0   0.5 0.3216450 0.07721204
## 5    0.1   1.0 0.5926407 0.07969758
## 6    1.0   1.0 0.3642857 0.08119400
## 7   10.0   1.0 0.3402597 0.08195816
## 8  100.0   1.0 0.3781385 0.08188350
## 9    0.1   2.0 0.5976190 0.08711625
## 10   1.0   2.0 0.3833333 0.09368097
## 11  10.0   2.0 0.3874459 0.08350728
## 12 100.0   2.0 0.3926407 0.08643085
plot(tune.model)

這張圖挺有意思的,根據顏色的深淺,可以看得出來有一層一層的山峰出現在圖中,表示在特定範圍內的cost和gamma,會有互相配合/影響的可能性。

而我們會希望classification error越小越好,也就是圖中的顏色越深越好。最後模型給出的最佳參數組合是:cost=10, gamma=0.5。

進一步,可以根據這張圖,縮小cost和gamma的範圍,再去去tune更多的參數組合。

最後,要挑出表現最佳的模型,可以直接從tune()回傳的結果中取出($best.model):

# Best model in set of tuning models
tune.model$best.model
## 
## Call:
## best.tune(method = svm, train.x = Type ~ ., data = data, ranges = list(cost = 10^(-1:2), 
##     gamma = c(0.5, 1, 2)), kernel = "radial")
## 
## 
## Parameters:
##    SVM-Type:  C-classification 
##  SVM-Kernel:  radial 
##        cost:  10 
##       gamma:  0.5 
## 
## Number of Support Vectors:  162


Tune parameters in SVR

在SVR中,一般會去調的參數是(cost, epsilon):

# data
data = data.frame(x=1:20,
                  y=c(3,4,8,2,6,10,12,13,15,14,17,18,20,17,21,22,25,30,29,31))

# tune cost and epsilon in SVR
tune.model = tune(svm,
                  y~x,
                  data=data,
                  range=list(cost=2^(2:9), epsilon = seq(0,1,0.1))# 調參數的最主要一行
)

這裡有兩個重點:

  • 在訓練的過程中,我們訓練的是一堆模型,包含cost=2^2., 2^3. … 2^9.,epsilon=0, 0.1, 0.2…1,這兩種參數的排列組合,換句話說,會有8x11=88個SVR模型。

  • 裡面的值是mean squared error

tune.model
## 
## Parameter tuning of 'svm':
## 
## - sampling method: 10-fold cross validation 
## 
## - best parameters:
##  cost epsilon
##     4       0
## 
## - best performance: 4.532176
plot(tune.model)

而這張圖的分層也很有意思,隨著epsilon越高,模型的表現越糟(顏色越淺)。同時它也希望cost比較低,所以最後得出的最佳參數組合: cost=4, epsilon=0

最後,要挑出表現最佳的模型,可以直接從tune()回傳的結果中取出($best.model):

# Best model in set of tuning models
tune.model$best.model
## 
## Call:
## best.tune(method = svm, train.x = y ~ x, data = data, ranges = list(cost = 2^(2:9), 
##     epsilon = seq(0, 1, 0.1)))
## 
## 
## Parameters:
##    SVM-Type:  eps-regression 
##  SVM-Kernel:  radial 
##        cost:  4 
##       gamma:  1 
##     epsilon:  0 
## 
## 
## Number of Support Vectors:  20

總結

SVM是資料科學中最重要的演算法之一,它同時具有機器學習和統計理論的長處,在分類及預測的問題上都表現得不錯。

但要充分發揮SVM/SVR的長處,需要將其中的許多細節(參數)知道得十分清楚,同時具有對資料的敏銳度才行。

網路上有不少資源,首推林軒田的「機器學習技法」,裡面完整介紹了SVM的細節與延伸,當然有很多數學,但也因此相當紮實,值得花時間好好靜下來學習。

(忘記說,在e1071套件裡的svm(),演算法便是 soft-margin SVM)

It’s still a long way to go~