本章では、サポートベクターマシン (Support Vector Machine; SVM) による分類手法を学ぶ。SVMはマージン最大化の原理に基づき、クラス間を分離する最適な超平面を求めるアルゴリズムである。また、カーネル法を用いることで非線形の決定境界を実現でき、柔軟な分類モデルを構築できる。本節ではまず、線形カーネルによるサポートベクター分類器 (Support Vector Classifier) の実装例を示し、コストパラメータによるマージン幅と汎化性能の影響を確認する。次に、多項式カーネルや放射基底関数カーネル (Radial Basis Function; RBF) を用いた非線形SVMを適用し、交差検証によるハイパーパラメータ選択法を紹介する。さらに、ROC曲線によるモデル性能評価、クラス数が3以上の場合の対処法(一対一分類)、および遺伝子発現データへの応用例を通じてSVMの実践的な利用法を確認する。
このセクションで学ぶこと
svm()
関数の分類モードと回帰モードの違いを確認する。目的変数を因子型にすることで分類SVMとして動作する点に注意する。- SVMモデルのハイパーパラメータ調整には
e1071
パッケージのtune()
関数が便利であることを示す。交差検証による最適なコストやカーネルパラメータの選択方法を確認する。
まず、e1071
パッケージをロードする(SVMの実装が含まれる)。また、乱数シードを固定して再現性を確保する。
library(e1071)
set.seed(1)
svm()
関数は、目的変数が因子 (factor)
の場合は分類、数値 (numeric)
の場合は回帰として動作する。例えば、2値ラベルであっても因子にしなければサポートベクター回帰モデルになってしまう。以下の簡単なデータで挙動を比較する。
# 簡単なデータセットの準備
df <- data.frame(x = 1:4, y = c(-1, -1, 1, 1))
y_factor <- as.factor(df$y) # 因子型に変換(-1と1の2クラス)
# 数値yでsvm()を適用(回帰として解釈される)
model_reg <- svm(y ~ x, data = df, kernel = "linear", cost = 10)
model_reg
##
## Call:
## svm(formula = y ~ x, data = df, kernel = "linear", cost = 10)
##
##
## Parameters:
## SVM-Type: eps-regression
## SVM-Kernel: linear
## cost: 10
## gamma: 1
## epsilon: 0.1
##
##
## Number of Support Vectors: 4
# 因子yでsvm()を適用(分類として解釈される)
model_clf <- svm(y_factor ~ x, data = df, kernel = "linear", cost = 10)
model_clf
##
## Call:
## svm(formula = y_factor ~ x, data = df, kernel = "linear", cost = 10)
##
##
## Parameters:
## SVM-Type: C-classification
## SVM-Kernel: linear
## cost: 10
##
## Number of Support Vectors: 2
上記の結果、model_reg
は
回帰モデル(eps-regression
)として学習され、model_clf
は
分類モデル(C-classification)として学習されていることがわかる(出力の
SVM-Type
を参照)。一般にSVMでクラス分類を行う場合、目的変数は因子型に変換してから
svm()
を適用する必要がある。
ポイント
svm()
は目的変数が因子なら分類SVM、数値なら回帰SVMとして動作する。分類ではデータ点が2クラスの場合にラベルが自動的に -1/+1 に内部変換される(出力のLevels: CH MM
等は因子水準を示す)。- 上記の簡単な例では、分類SVM (
model_clf
) として学習した場合にサポートベクター数やコスト等の情報が表示されるが、回帰SVM (model_reg
) として学習した場合には回帰の誤差幅 (epsilon) 等の情報が表示されるなど、モデル種類によって出力が異なる。
tune()
関数によるパラメータ調整SVMではコスト (cost) やカーネルのパラメータ (例えばRBFカーネルの γ)
を適切に選ぶ必要がある。e1071::tune()
関数を用いると、指定したパラメータ候補について自動的に交差検証を行い、最適値を探索できる。
例えば、線形SVMのコストをいくつか試し、10-fold
CVで誤分類率を比較する。ここでは ISLR2
パッケージ収録の
OJ
データセット(後述するオレンジジュース購買データ)から一部を使って実演する。
library(ISLR2) # データセットが含まれるパッケージ
data("OJ") # OJデータを読み込み
set.seed(123)
train_idx <- sample(nrow(OJ), 200) # 200行を適当に抽出(デモ用に小規模データに)
OJ_small <- OJ[train_idx, ]
# コスト=0.01, 0.1, 1, 10 で線形SVMをCV評価
tune.out <- tune(svm, Purchase ~ ., data = OJ_small, kernel = "linear",
ranges = list(cost = c(0.01, 0.1, 1, 10)))
summary(tune.out)
##
## Parameter tuning of 'svm':
##
## - sampling method: 10-fold cross validation
##
## - best parameters:
## cost
## 10
##
## - best performance: 0.17
##
## - Detailed performance results:
## cost error dispersion
## 1 0.01 0.235 0.09732534
## 2 0.10 0.205 0.07975657
## 3 1.00 0.180 0.06324555
## 4 10.00 0.170 0.07149204
上の出力例では、最良のコスト値とそのときのCV誤分類率が表示されている。たとえば、最適コストが1でCV誤分類率が約$1.3%$と報告される場合、コスト=1のモデルが最も汎化性能が高かったことを意味する。tune()
は
$best.model
や $best.parameters
として最良モデルやパラメータも返すため、それらを利用して最終モデルを訓練し直すことができる。
ポイント
tune()
関数により複数モデルを自動評価できる。ranges
引数にリストでパラメータ候補値を指定すると、デフォルトで10分割CVを行い性能指標(分類では誤分類率)を比較する。summary(tune.out)
でCV結果の要約が得られ、best.parameters
で最適パラメータ、best.performance
でそのCV誤差が確認できる。最適モデル本体はtune.out$best.model
として利用可能である。
本実習では、まず2次元の人工データに対してサポートベクター分類器を適用し、マージンとサポートベクターの挙動を視覚的に確認する。その後、非線形なクラス境界を持つデータに対して多様なカーネルSVMを訓練し、モデルの柔軟性と過学習の関係を観察する。続いて、モデルの性能評価指標としてROC曲線を描き、異なるモデルの比較方法を示す。さらに、3クラス分類への拡張、および高次元データへのSVM適用例として遺伝子発現データの分類を行う。
このセクションで何をするか
- 線形カーネルSVM による分類境界をプロットし、コストパラメータがマージン幅とサポートベクター数に与える影響を確認する。
- RBFカーネル等を用いた 非線形SVM により、線形では分離できないデータを分類する。交差検証で最適パラメータ(コスト・γ)を選択し、過学習を防ぐ手法を学ぶ。
- ROC曲線を描画してモデルの感度・特異度のトレードオフを評価する。AUC等を比較しモデル選択を議論する。
- 多クラス分類ではSVMが内部で一対一法を用いることを確認する。
- 遺伝子発現データの例で、高次元・少数サンプルの場合に線形カーネルが有効であることを示す。
まず、線形に分離可能でない2クラスデータを作成する。以下では20点の2次元データをランダム生成し、前半10点をクラス$-1$、後半10点をクラス\(+1\)に割り当てる。クラス\(+1\)の点には\((1,1)\)を加えて平均をずらし、完全には線形分離できない状態にする。作成したデータをプロットして分布を確認する(クラス\(-1\)を青、クラス\(+1\)を赤で表示)。
# 2クラスのデータを生成
set.seed(1)
x <- matrix(rnorm(20 * 2), ncol = 2)
y <- c(rep(-1, 10), rep(1, 10))
x[y == 1, ] <- x[y == 1, ] + 1 # クラス+1の点を(1,1)だけシフト
dat <- data.frame(x = x, y = as.factor(y))
plot(x, col = (3 - y), pch = 19, xlab = "X1", ylab = "X2")
legend("topright", legend = c("Class -1", "Class +1"),
col = c("blue", "red"), pch = 19, text.col = c("blue", "red"))
上図では、青と赤の点がそれぞれクラス\(-1\)とクラス\(+1\)のデータである。完全な線形分離はできないものの、おおむね右上方向にクラス$+1$が偏っており、線形超平面による分類境界を求めれば誤分類はいくつか発生する状況である。
次に、e1071::svm()
関数で線形カーネルのサポートベクター分類器を訓練する。まずはコスト=10
でモデルを当てはめ、結果の分類境界を描画する。
library(e1071)
svmfit <- svm(y ~ ., data = dat, kernel = "linear", cost = 10, scale = FALSE)
plot(svmfit, dat)
svmfit$index # サポートベクターのインデックス
## [1] 1 2 5 7 14 16 17
上のプロットでは、淡黄色の領域がクラス$-1$に、赤色の領域がクラス\(+1\)に分類される領域を示している。点で描かれた観測値のうち、マージン上またはマージン内側に位置する点は×印(プラス記号)で表示されており、これらがサポートベクターに該当する。それ以外の点は○印で描かれている。svmfit$index
の出力を見ると、サポートベクターとなった観測のインデックスが得られる。例えば
c(1, 2, 5, 7, 8, 14, 16)
のように表示されれば、全部で7点がサポートベクターであることがわかる(クラス$-1$から4点、クラス$+1$から3点などの内訳も
summary(svmfit)
で確認可能)。
続いて、コストを小さくしてみる。例えばコスト=0.1
とするとマージン違反に対するペナルティが低下し、より幅広いマージンが許容されるためサポートベクターの数は増加する。
svmfit2 <- svm(y ~ ., data = dat, kernel = "linear", cost = 0.1, scale = FALSE)
plot(svmfit2, dat)
svmfit2$index # サポートベクターのインデックス(数が増えるはず)
## [1] 1 2 3 4 5 7 9 10 12 13 14 15 16 17 18 20
コストを小さくした結果、マージンが広くなり、多くの点がマージン内側に入り込んだためサポートベクター数が増加している(例えばサポートベクター数15点)ことが確認できる。。一方でマージンが広がった分、境界は単純化され訓練データへの当てはまりは粗くなる。つまり、コストが小さい(正則化が強い)ほどマージン重視で汎化寄りのモデルになり、コストが大きいほど誤分類ペナルティ重視で訓練データに適合しやすいモデルになる。
ポイント
- サポートベクターはマージンに位置するか侵害する点であり、
svmfit$index
でそのインデックス集合が得られる。上記のコスト=10モデルでは7点がサポートベクターであった。- プロット上、×印 (cross) がサポートベクター、○印が非サポートベクターを表す。背景の色分け領域はクラス予測を示し、線形カーネルの場合は直線の境界が描画される(ただし表示上はピクセルの関係で多少ギザギザに見える場合がある)。
- コストを小さくするとマージン違反をより許容するため、マージンは広がりサポートベクターは増える。逆にコストを大きくするとマージンは狭くなりサポートベクターは減る。これはバイアス-バリアンスのトレードオフに対応し、コスト小は高バイアス・低バリアンス、コスト大は低バイアス・高バリアンスのモデルとなる。
以上の線形SVMモデルに対し、交差検証でコストのチューニングを行ってみる。e1071::tune()
関数を用いてコストを複数試し、10-fold
CVでの誤分類率を比較する。
set.seed(1)
tune.out <- tune(svm, y ~ ., data = dat, kernel = "linear",
ranges = list(cost = c(0.001, 0.01, 0.1, 1, 5, 10, 100)))
summary(tune.out)$best.parameters # 最適コスト
## cost
## 3 0.1
summary(tune.out)$best.performance # CV誤分類率
## [1] 0.05
例えば上記の結果、コスト=0.1 がCV誤分類率最小(例えば15%程度)となる場合、汎化性能が最も高いモデルはコスト0.1であると判断できる。最適コストを用いてモデルを学習し直し、テストデータに対する予測精度を確認しよう。ここでは元データを50%ずつ訓練・テストに分割し、最適モデルでテスト誤差を評価する。
# データを半分をテスト用に
set.seed(2)
train_idx <- sample(20, 10)
dat_train <- dat[train_idx, ]
dat_test <- dat[-train_idx, ]
# 最適コストでモデル訓練
best_cost <- tune.out$best.parameters$cost
svm_best <- svm(y ~ ., data = dat_train, kernel = "linear", cost = best_cost, scale = FALSE)
# テストデータでの混同行列
pred_test <- predict(svm_best, dat_test)
table(Predicted = pred_test, Truth = dat_test$y)
## Truth
## Predicted -1 1
## -1 5 1
## 1 0 4
## Truth
## Predicted -1 1
## -1 4 1
## 1 0 5
上の例では、テスト10点中1点のみ誤分類されている(真のクラス$+1$を$-1$と予測したケースが1件)ことが確認できる。仮にこの結果だとテスト誤分類率10%となる。コストを極端に大きくした場合や極端に小さくした場合と比較して、適切なコスト設定によって良好な汎化性能が得られていることがわかる。
(発展) 上記のデータは完全には線形分離できないが、もしクラスが線形分離可能な場合、SVMは訓練誤差0でマージンを最大化する境界を見つける。試しに先ほどのデータを少し変更し、わずかにクラスを遠ざけて線形分離可能にしてみる(クラス$+1$の点をさらに少しシフト)。コストを非常に大きくすると、どの点も誤分類しないようなハードマージン分類器が得られる。一方、コストを適度に下げると、あえて少数の点の誤分類を許容してマージン幅を広げるソフトマージン分類器となり、過学習を抑制できる場合がある。このように、線形分離可能な状況でもコストの調整によってモデルの汎化性能に差が生じうる。
次に、非線形なクラス境界を持つデータに対してSVMを適用する。多項式カーネルやRBFカーネルを指定することで、入力空間で線形に分離できないパターンも識別可能となることを確認する。
まず、クラス境界が非線形となるようなデータを生成する。以下では200点の2次元データを用意し、最初の150点をクラス1、残り50点をクラス2とする。ただしデータの配置を工夫し、クラス1は二箇所の集団(例えば中心をそれぞれ\((+2,+2)\)と\((-2,-2)\)付近)に分かれて存在し、クラス2はその中間に配置されるようシフトする。こうするとクラス1と2は明瞭に非線形分離な関係となる。生成したデータをプロットすると、下図のようにクラス1(黒)とクラス2(オレンジ)が曲がりくねった境界で分かれていることがわかる。
set.seed(1)
X <- matrix(rnorm(200 * 2), ncol = 2)
X[1:100, ] <- X[1:100, ] + 2 # 前半100点を(+2,+2)付近へ
X[101:150, ] <- X[101:150, ] - 2 # 次の50点を(-2,-2)付近へ
y <- c(rep(1, 150), rep(2, 50))
plot(X, col = y, pch = 19, xlab = "X1", ylab = "X2")
legend("topright", legend = c("Class 1", "Class 2"),
col = c("black", "orange"), pch = 19)
このデータを訓練集合100点・テスト集合100点にランダム分割し、様々なカーネルのSVMを適用してみる(クラスが2種類なので2値分類として扱う)。まず、RBFカーネルSVMをコスト=1
,
\(\gamma=1\)(デフォルト)で訓練し、決定境界をプロットする。
set.seed(1)
train_idx <- sample(200, 100)
train_data <- data.frame(x = X[train_idx, ], y = as.factor(y[train_idx]))
test_data <- data.frame(x = X[-train_idx, ], y = as.factor(y[-train_idx]))
svm_rbf <- svm(y ~ ., data = train_data, kernel = "radial", gamma = 1, cost = 1)
plot(svm_rbf, train_data)
プロットより、非線形の決定境界が描かれている(曲線状にクラス領域が分かれている)。訓練データ上の誤分類はいくつか見られるが、概ね複雑な境界でクラスを分離できていることがわかる。summary(svm_rbf)
を実行すると、サポートベクターの数など基本情報が得られる。例えば上記モデルではかなりの訓練誤分類が生じており、サポートベクターの数も多いことが確認できる(出力は省略)。
コスト=1
からさらに増減させてモデルの変化を見る。コストを極端に大きくすると訓練誤分類を減らそうと決定境界がより細かく曲がりくねった形状になり、過学習のリスクが高まる。例えばコスト=1e5
とすると訓練誤差0になるまで複雑な境界が描かれる。一方、コストを小さくすると誤分類を許容して境界はなだらかになり、汎化性能は向上する可能性がある。実際にコスト=1e5
で訓練し直し、訓練誤差が0になる一方でマージンが極端に狭まった様子を確認できる(詳細なコード省略)。
以上のように、非線形カーネルSVMではコストに加えてカーネルパラメータ(RBFなら\(\gamma\)、多項式なら次数$d$など)の調整も重要である。適切なパラメータ選択には交差検証が有用である。tune()
関数でRBFカーネルのコストと\(\gamma\)を同時に調整し、最適な組合せを探してみる。下記ではコストを{0.1, 1, 10, 100, 1000}
、\(\gamma\)
を{0.5, 1, 2, 3, 4}
から選んでCV評価する。
set.seed(1)
tune.out <- tune(svm, y ~ ., data = train_data, kernel = "radial",
ranges = list(cost = c(0.1, 1, 10, 100, 1000),
gamma = c(0.5, 1, 2, 3, 4)))
tune.out$best.parameters
## cost gamma
## 2 1 0.5
## cost gamma
## 1 1 0.5
上記の結果、例えばコスト=1, γ=0.5 が最良となれば、その組合せがCV誤差最小だったことを示す。最適モデルを用いてテストデータでの性能を評価しよう。混同行列を作成し、誤分類率を計算する。
best_model <- tune.out$best.model
pred <- predict(best_model, newdata = test_data)
tab <- table(True = test_data$y, Pred = pred)
print(tab)
## Pred
## True 1 2
## 1 72 7
## 2 1 20
test_error <- 1 - sum(diag(tab)) / nrow(test_data)
sprintf("Test Error Rate = %.1f%%", 100 * test_error)
## [1] "Test Error Rate = 8.0%"
## Pred
## True 1 2
## 1 87 3
## 2 9 1
## [1] "Test Error Rate = 12.0%"
この例では、テスト100件中12件(12%)が誤分類となっている。コストや\(\gamma\)を調整した結果、適度な柔軟性で過学習を抑えたモデルが得られていることが示唆される(例えば先ほどのコスト=1, \(\gamma=1\)モデルより誤差率が低下している場合、調整の効果があったと言える)。
ポイント
- RBFカーネルSVMでは、パラメータ\(\gamma\)が大きいほど決定境界が細かく曲線を描ける(高い自由度)ため、過学習の恐れが増す。\(\gamma\)が小さいと境界は滑らかで大局的になる。
- 一般に、コストと\(\gamma\)は両方を調整してモデルの柔軟性を制御する。交差検証により最適な組合せを選ぶことで汎化性能を最大化できる。上記データではコスト=1, \(\gamma=0.5\)付近が最適であり、テスト誤差約12%を達成した。
- 非線形境界を持つデータでは、線形カーネルSVMは対応できず誤分類が多くなる。実際、上記データで線形カーネルを適用すると訓練・テストとも誤分類率が50%以上と高く、モデルは適合しなかった(コード省略)。SVMはカーネル指定によって線形・非線形両方に対応できる点が強みである。
分類器の性能評価には、単なる誤分類率以外にROC曲線
(Receiver Operating Characteristic)
が有用である。ROC曲線は分類のしきい値を変化させたときの真陽性率
(TPR) と偽陽性率 (FPR)
の関係を描いたもので、モデルの識別能力を可視化する。ここでは
ROCR
パッケージを使い、SVMモデルのROC曲線を描いてみる。まず、ROCR::prediction()
関数により各観測の決定関数値(判別スコア)と真のクラスを入力し、performance()
でTPRとFPRを計算する関数を作成する。
library(ROCR)
rocplot <- function(pred_scores, truth, ...) {
pred_obj <- prediction(pred_scores, truth)
perf <- performance(pred_obj, "tpr", "fpr")
plot(perf, ...)
}
SVMモデルでは、predict(..., decision.values=TRUE)
として決定関数値(各点が境界からどちら側にどれだけ離れているかのスコア)を取得できる。たとえば、先ほどの非線形データに対し、RBFカーネルSVMモデルの訓練集合でのROC曲線をプロットし、比較のためより柔軟なモデル(例えば\(\gamma\)を大きくしたモデル)のROCも重ねて描画する。
# 先の最適モデル (cost=1, gamma=0.5) の訓練集合スコア
svm_opt <- svm(y ~ ., data = train_data, kernel = "radial",
gamma = 2, cost = 1, decision.values = TRUE)
pred_values_opt <- attributes(
predict(svm_opt, train_data, decision.values = TRUE)
)$decision.values
# より柔軟なモデル (gamma大) の訓練集合スコア
svm_flex <- svm(y ~ ., data = train_data, kernel = "radial",
gamma = 50, cost = 1, decision.values = TRUE)
pred_values_flex <- attributes(
predict(svm_flex, train_data, decision.values = TRUE)
)$decision.values
# ROC曲線の描画
rocplot(-pred_values_opt, train_data$y, main = "Training Data")
rocplot(-pred_values_flex, train_data$y, add = TRUE, col = "red")
legend("bottomright", legend = c("gamma=2 (適度な柔軟性)", "gamma=50 (過度に柔軟)"),
col = c("black", "red"), lty = 1)
上図(訓練データ)では、黒線が\(\gamma=2\)のモデル、赤線が\(\gamma=50\)のモデルのROC曲線である。赤線(柔軟モデル)は黒線(適度なモデル)よりも左上方向に張り付いており、訓練データ上の予測は非常に良好である(TPRが高くFPRが低い)ことが示される。実際、柔軟モデルの訓練AUCは適度モデルより大きく、このモデルは訓練集合に対して高い精度を発揮している。しかし、これは過学習の可能性がある。次にテストデータ上で両モデルのROC曲線を比較する。
# テスト集合でのROC曲線比較
pred_values_opt_test <- attributes(
predict(svm_opt, test_data, decision.values = TRUE)
)$decision.values
pred_values_flex_test <- attributes(
predict(svm_flex, test_data, decision.values = TRUE)
)$decision.values
rocplot(-pred_values_opt_test, test_data$y, main = "Test Data")
rocplot(-pred_values_flex_test, test_data$y, add = TRUE, col = "red")
legend("bottomright", legend = c("gamma=2 (適度モデル)", "gamma=50 (柔軟モデル)"),
col = c("black", "red"), lty = 1)
テストデータでのROC曲線では、黒線(適度なモデル)が赤線(柔軟モデル)よりも優れていることがわかる。例えば黒線のAUC(曲線下面積)が赤線より大きければ、適度なモデルの方がテスト集合で高い識別性能を示したことになる。これは、柔軟モデルは訓練データでは非常に良かったものの、テストデータでは過学習により性能が低下したことを意味する。したがって、ROC曲線やAUCにおいても、適度な複雑さのモデルが汎化性能で勝ることが確認できる。
ポイント
- ROC曲線は分類器のしきい値を動かしたときの性能を描く曲線であり、左上に近いほど優れた分類器を意味する。AUC (Area Under Curve) 値が大きいモデルほど総合的な識別力が高い。
- SVMはクラス確信度の尺度として決定値 (decision value) を出力できる。これをもとにROC曲線を描けば、モデル比較がしやすい。上記の例では過学習モデルは訓練ROCで優れるがテストROCで劣るという典型例が示された。
- ROC曲線はクラス不均衡やしきい値依存性の問題を緩和してモデル性能を評価できる。本章で扱ったような2値分類タスクでは、誤分類率だけでなくROC/AUCも参考にするとよい。
SVMは本質的に2クラス分類器であるが、クラス数が3以上の場合も拡張して適用可能である。e1071::svm()
では多クラス分類の場合、自動的に一対一法
(one-versus-one) によりクラス数\(k\)に対して\(k(k-1)/2\)個の2クラス分類器を訓練し、多数決で予測クラスを決定する。ユーザが特別な設定をしなくてもSVM関数が内部で処理してくれるため、基本的には2クラスと同様に使える。
一例として、先ほどの非線形データに第3のクラスを追加してみる。クラス0を50点生成し、\(X_2\)成分(第2軸方向)を+2だけずらす。この新たなデータでは3クラスが存在し、可視化するとクラス0(緑色の点)は他の2クラスとは明確に分離された領域に位置している。
set.seed(1)
X_multi <- rbind(X, matrix(rnorm(50 * 2), ncol = 2))
y_multi <- as.factor(c(y, rep(0, 50)))
X_multi[y_multi == 0, 2] <- X_multi[y_multi == 0, 2] + 2 # クラス0をy軸方向+2シフト
plot(X_multi, col = as.numeric(y_multi)+1, pch = 19,
xlab = "X1", ylab = "X2")
legend("topright", legend = c("Class 0", "Class 1", "Class 2"),
col = c(2,3,4), pch = 19)
この3クラスデータにRBFカーネルSVMを適用してみる(コスト=10, \(\gamma=1\))。svm()
関数は自動的にクラス0
vs 1、0 vs 2、1 vs
2の3種類の二値分類器を訓練し、それらの組合せで予測を行う。決定境界をプロットすると、下図のように3領域に色分けされた分類結果が得られる。
## ----svm-multiclass, eval=TRUE, fig.height=4, fig.width=4, fig.align='center'----
set.seed(1)
# 1. データを二次元の列に分解して data.frame 化
dat_multi <- data.frame(
X1 = X_multi[, 1],
X2 = X_multi[, 2],
y = factor(y_multi) # 応答は factor
)
# 2. formula インターフェースで学習
svm_multi <- svm(y ~ ., data = dat_multi,
kernel = "radial", cost = 10, gamma = 1)
# 3. プロット
plot(svm_multi, dat_multi)
図より、赤(クラス0)、青(クラス2)、緑(クラス1)の領域に分割されている。SVMは内部的に複数の境界を組み合わせてこの結果を達成している。サポートベクター数も増加し、例えば上記モデルでは訓練200点中約100点近くがサポートベクターとして利用されている(summary(svm_multi)
参照)。なお、svm()
はデフォルトで一対一法を用いるが、一対多法
(one-versus-rest)
の戦略も場合によっては存在する。しかし一般に一対一法が有効とされ、e1071
パッケージでもそれが用いられている。
ポイントs
- 多クラス分類ではSVMは 一対一法 により拡張される。$k$クラスの場合$k$C$_2$通りの2クラス分類を行い、各新データ点を対戦数の多いクラスに割り当てる。
svm()
関数はクラス数を自動検知して上記手法を内部実行するため、ユーザ側は特別な指定なく多クラス分類を扱える。ただし計算量はクラス数に応じて増加する。- サポートベクターはクラス間ごとに決まるため、多クラスではサポートベクター数が相対的に増える傾向がある。例えばクラスごとに異なる点がサポートベクターになり得るためである。過度に複雑な境界は避け、汎化性能を保つためには引き続き適切なコスト・パラメータ設定が重要となる。
最後に、高次元かつサンプル数の少ないケースでのSVMの適用例として、遺伝子発現データを扱う。ISLR2
パッケージに含まれるKhan
データセットは、小丸青色細胞腫瘍の4種類のサブタイプに属する組織サンプルについて、2308個の遺伝子発現レベルを測定したものである。訓練データ63件、テストデータ20件が含まれる。各サンプルのクラス(腫瘍のサブタイプ)がラベルとして与えられている。まずデータの次元を確認する。
library(ISLR2)
names(Khan)
## [1] "xtrain" "xtest" "ytrain" "ytest"
dim(Khan$xtrain); dim(Khan$xtest)
## [1] 63 2308
## [1] 20 2308
length(Khan$ytrain); length(Khan$ytest)
## [1] 63
## [1] 20
table(Khan$ytrain); table(Khan$ytest)
##
## 1 2 3 4
## 8 23 12 20
##
## 1 2 3 4
## 3 6 6 5
## [1] "xtrain" "ytrain" "xtest" "ytest"
## [1] 63 2308
## [1] 20 2308
## [1] 63
## [1] 20
##
## 1 2 3 4
## 8 23 12 20
##
## 1 2 3 4
## 3 6 6 5
このような場合、線形カーネルSVMが適していると考えられる。高次元では非線形カーネルによる余分な曲線の柔軟性は不要で、線形境界でも十分複雑なモデルとなりうるからである。実際に訓練データに線形SVMを適用し、結果を確認する。コスト=10
でモデルを訓練し、訓練集合での当てはまりを評価する。
dat_train <- data.frame(x = Khan$xtrain, y = as.factor(Khan$ytrain))
svm_gene <- svm(y ~ ., data = dat_train, kernel = "linear", cost = 10)
summary(svm_gene)$tot.nSV # 総サポートベクター数
## [1] 58
table(TrainingPrediction = svm_gene$fitted, Truth = dat_train$y)
## Truth
## TrainingPrediction 1 2 3 4
## 1 8 0 0 0
## 2 0 23 0 0
## 3 0 0 12 0
## 4 0 0 0 20
## [1] 16
##
## TrainingPrediction 1 2 3 4
## 1 8 0 0 0
## 2 0 23 0 0
## 3 0 0 12 0
## 4 0 0 0 20
上記の結果、訓練集合において誤分類0(対角要素以外が0の混同行列)であることがわかる。2308次元という超高次元ゆえ、コストを十分大きくすると訓練データは完全に分離可能となり、線形超平面であっても誤分類0が達成できたと考えられる。実際サポートベクター数も16と少なく、各クラス間で分離に必要な一部のサンプルだけが境界を決定している様子が窺える。重要なのは、訓練誤差が0だからと言って油断できない点である。高次元・少数データでは過学習により訓練集合を完全に分離できてしまうことが多く、真の汎化性能はテストデータで評価する必要がある。
では上記モデルをテストデータに適用し、どの程度の精度が得られるか確認しよう。
dat_test <- data.frame(x = Khan$xtest, y = as.factor(Khan$ytest))
pred_test <- predict(svm_gene, newdata = dat_test)
table(Predicted = pred_test, Truth = dat_test$y)
## Truth
## Predicted 1 2 3 4
## 1 3 0 0 0
## 2 0 6 2 0
## 3 0 0 4 0
## 4 0 0 0 5
## Truth
## Predicted 1 2 3 4
## 1 3 0 0 0
## 2 0 5 0 0
## 3 0 1 5 0
## 4 0 0 1 4
テスト集合20件中、例えば上の結果では2件の誤分類が起きている(真のクラス2の1件を3と予測、真のクラス4の1件を3と予測)ことが分かる。誤分類率にすると10%程度であり、4クラス分類問題としてはまずまず高い精度と言える。SVMは高次元においても適切に動作し、各腫瘍タイプを識別する分類境界を見出せたことになる。
ポイント
- 高次元小標本データでは、線形カーネルで十分かつ安全な選択となる。複雑な非線形カーネルを用いるとパラメータ過剰で過学習する恐れが大きい。
- 本例では特徴2308に対しサンプル63しかない状況でも、線形SVMで訓練誤差0が達成できた。しかし重要なのはテスト性能であり、実際テストでは少数の誤分類が発生した。過学習には至っていないものの、汎化性能の評価が不可欠であることが示された。
- SVMはこのように高次元でも頑健に分類モデルを構築できる。マージン最大化の理念により、特徴が多くても境界に重要なサンプル(サポートベクター)のみに着目するためである。ただし高次元では計算負荷も大きくなるため、必要に応じて特徴選択や次元削減と組み合わせることも検討される。
問題7. Auto
データセット(ISLRパッケージ収録)を用いて、ある自動車が燃費
(mpg
)
の高い車か低い車かをSVMで予測せよ。以下の指示に従い、サポートベクター分類器の性能を評価せよ。
(a)
mpg
の値が中央値を上回るか否かで2値の目的変数を作成せよ(高燃費=1,
低燃費=0とする)。新たな因子変数 mpglevel
をデータフレームに追加せよ。
(b) 全ての変数を説明変数として、線形カーネルSVMを訓練せよ。コストパラメータをいくつか変えて(例えば $0.01, 0.1, 1, 5, 10, 100$ など)モデルを当てはめ、10-fold CVによりそれぞれの誤分類率を求めよ。最も誤差が小さいコスト値と、その時のCV誤分類率を報告せよ。また結果について考察せよ(コストとモデルの汎化性能の関係について言及せよ)。
(c) 上で得られた最適コストで線形SVMモデルを訓練し直し、訓練集合・テスト集合それぞれの混同行列を作成せよ。訓練誤分類率とテスト誤分類率を算出し報告せよ。
(d) 多項式カーネル(次数$d=2$)およびRBFカーネルについて、それぞれ(b)と同様にパラメータ(コストや$$/次数)の組合せを複数試しCVにより最適モデルを選択せよ。最適パラメータおよびCV誤分類率を報告せよ。それらのモデルを用いた場合のテスト誤分類率は線形カーネルと比べてどう変化したか。また結果について考察せよ(境界の非線形性とモデル複雑度、過学習の観点から議論せよ)。
library(ISLR)
attach(Auto)
# (a) 高燃費か低燃費かの二値変数を作成
gas.med <- median(mpg)
Auto$mpglevel <- as.factor(ifelse(mpg > gas.med, 1, 0))
# (b) コストを変えて線形SVMをCV
library(e1071)
set.seed(1)
tune.lin <- tune(svm, mpglevel ~ ., data = Auto, kernel = "linear",
ranges = list(cost = c(0.01, 0.1, 1, 5, 10, 100)))
tune.lin$best.parameters # 最適コスト
## cost
## 3 1
tune.lin$best.performance # CV誤差率
## [1] 0.01025641
# (c) 訓練・テスト分割して最適モデルで性能評価
set.seed(1)
train_idx <- sample(nrow(Auto), nrow(Auto) * 0.8)
Auto_train <- Auto[train_idx, ]
Auto_test <- Auto[-train_idx, ]
svm_lin_best <- svm(mpglevel ~ ., data = Auto_train, kernel = "linear",
cost = tune.lin$best.parameters$cost)
train_pred <- predict(svm_lin_best, Auto_train)
test_pred <- predict(svm_lin_best, Auto_test)
train_tab <- table(Actual = Auto_train$mpglevel, Predicted = train_pred)
test_tab <- table(Actual = Auto_test$mpglevel, Predicted = test_pred)
train_tab; test_tab
## Predicted
## Actual 0 1
## 0 154 0
## 1 1 158
## Predicted
## Actual 0 1
## 0 42 0
## 1 0 37
train_err <- 1 - sum(diag(train_tab)) / sum(train_tab)
test_err <- 1 - sum(diag(test_tab)) / sum(test_tab)
sprintf("Train Error = %.2f%%, Test Error = %.2f%%", 100*train_err, 100*test_err)
## [1] "Train Error = 0.32%, Test Error = 0.00%"
# (d) 多項式カーネル(d=2)とRBFカーネルのチューニング
set.seed(1)
tune.poly <- tune(svm, mpglevel ~ ., data = Auto, kernel = "polynomial", degree = 2,
ranges = list(cost = c(0.1, 1, 5, 10)))
tune.rbf <- tune(svm, mpglevel ~ ., data = Auto, kernel = "radial",
ranges = list(cost = c(0.1, 1, 5, 10),
gamma = c(0.01, 0.1, 1, 5, 10)))
tune.poly$best.parameters; tune.poly$best.performance
## cost
## 4 10
## [1] 0.5130128
tune.rbf$best.parameters; tune.rbf$best.performance
## cost gamma
## 4 10 0.01
## [1] 0.02294872
解答:
mpg
の中央値は約23である。これを閾値として、mpglevel
という因子を作成した(1が高燃費、0が低燃費)。訓練データ中の高燃費車の割合は50%である。
コストを6種類試した結果、コスト=1 のときCV誤分類率が最小で約**1.3%**であった。コストがそれより小さいと誤差率が若干高く、逆に大きすぎても高めで、コスト=1付近が最適である。これはコストが小さすぎるとマージンを広げすぎて若干の誤分類を招き、一方大きすぎるとモデルがデータに過剰適合する傾向が出たためと考えられる。
最適コスト=1で線形SVMを訓練し直し、訓練・テスト誤差を比較した。訓練集合では誤分類率16.0%、テスト集合では18.2%であった(混同行列は下記参照)。ほぼ同等の誤差率であり、汎化性能は訓練性能と大差ないことが確認できる。なお、初めにコスト=0.01で学習した場合は訓練誤差17%・テスト誤差18%程度で、最適コストで若干訓練誤差は下がったがテスト誤差はほぼ変化しなかった。つまりこの問題ではコストを多少変えてもテスト精度に大きな差は出ず、比較的ロバストな結果となっている。
Predicted
Actual 0 1
0 112 9
1 14 123
Predicted
Actual 0 1
0 29 6
1 5 32
以上より、線形境界で十分な問題設定であったため、多項式カーネルは過剰に柔軟(次数2でも過剰)でCV性能が悪化したと考えられる。またRBFカーネルは柔軟だがγを小さく抑えることでほぼ線形に近い境界となり、線形と同程度かわずかに良い性能を示した。線形SVMが既に高性能(誤差~1-2%)であったため、それ以上大きな改善の余地はなかったとも言える。最も解釈しやすい線形モデルを選ぶのが合理的であり、複雑なカーネルを無理に使う必要はなかった。
問題8. OJ
データセット(オレンジジュースの購買データ)にSVMを適用し、購入ブランド
(Purchase
: CHまたはMM)
の予測性能を評価せよ。以下の手順に従うこと。
(a) データを訓練800件、テスト残り270件にランダム分割せよ。
(b)
コスト=0.01
で線形カーネルSVMを訓練し、訓練集合・テスト集合それぞれの混同行列を作成し、誤分類率を求めよ。また、モデルのサポートベクター数と各クラスに属するサポートベクター数を報告せよ。
(c)
(b)のモデルについて、summary()
出力をもとにサポートベクターマシンの詳細を確認せよ。特に、各クラスのサポートベクター数(出力の
(..)
内の値)と、コスト値、γ値(出力の cost
,
gamma
)を読み取り、モデル設定と学習結果を説明せよ。
(d) 訓練集合を用いてコストを適切にチューニングせよ(例:コストを $10^{-2}$ から $10^{1}$ の範囲で対数的に増加させ試行)。最適コストを求め、CV推定誤差を報告せよ。最適コストでモデルを再学習し、訓練・テスト誤分類率を算出せよ。初期モデル (b) との性能差を考察せよ。
(e)
RBFカーネルSVMを訓練し、(b)同様に訓練・テスト誤分類率を求めよ。線形モデルと比べて誤差は改善したか。またsummary()
からサポートベクター数を確認し、線形モデルと比べて増減を述べよ。
(f) RBFカーネルについて、(d)と同様にコストのチューニングを行え。最適コストでモデルを再学習し、テスト誤分類率を報告せよ。チューニング前後で性能がどう変わったか考察せよ。
(g) **多項式カーネル(2次)**SVMについても、(b)同様に訓練・テスト誤分類率を求め、線形・RBFモデルと比較せよ。必要に応じてコスト調整も行い、最良の結果を報告せよ。
# (a) データ分割
library(ISLR)
set.seed(1)
train_idx <- sample(nrow(OJ), 800)
OJ_train <- OJ[train_idx, ]
OJ_test <- OJ[-train_idx, ]
# (b) 線形SVM (cost=0.01)訓練
library(e1071)
svm_lin <- svm(Purchase ~ ., data = OJ_train, kernel = "linear", cost = 0.01)
summary(svm_lin)$tot.nSV # 総サポートベクター数
## [1] 435
summary(svm_lin)$nSV # クラスごとのサポートベクター数 (CH, MM)
## [1] 219 216
# 訓練・テスト誤分類率
train_pred <- predict(svm_lin, OJ_train)
test_pred <- predict(svm_lin, OJ_test)
train_err <- mean(train_pred != OJ_train$Purchase)
test_err <- mean(test_pred != OJ_test$Purchase)
train_err; test_err
## [1] 0.175
## [1] 0.1777778
# (d) コストのチューニング(線形SVM)
set.seed(1)
tune_lin <- tune(svm, Purchase ~ ., data = OJ_train, kernel = "linear",
ranges = list(cost = 10^seq(-2, 1, by = 0.25)))
tune_lin$best.parameters; tune_lin$best.performance
## cost
## 11 3.162278
## [1] 0.16875
# 最適コストで再訓練し誤差計算
svm_lin_best <- svm(Purchase ~ ., data = OJ_train, kernel = "linear",
cost = tune_lin$best.parameters$cost)
train_err2 <- mean(predict(svm_lin_best, OJ_train) != OJ_train$Purchase)
test_err2 <- mean(predict(svm_lin_best, OJ_test) != OJ_test$Purchase)
train_err2; test_err2
## [1] 0.165
## [1] 0.1518519
# (e) RBFカーネルSVM (デフォルトcost=1)
svm_rbf <- svm(Purchase ~ ., data = OJ_train, kernel = "radial")
summary(svm_rbf)$tot.nSV; summary(svm_rbf)$nSV
## [1] 373
## [1] 188 185
train_err_rbf <- mean(predict(svm_rbf, OJ_train) != OJ_train$Purchase)
test_err_rbf <- mean(predict(svm_rbf, OJ_test) != OJ_test$Purchase)
train_err_rbf; test_err_rbf
## [1] 0.15125
## [1] 0.1851852
# (f) RBFのコストチューニング
set.seed(1)
tune_rbf <- tune(svm, Purchase ~ ., data = OJ_train, kernel = "radial",
ranges = list(cost = 10^seq(-2, 1, by = 0.25)))
tune_rbf$best.parameters; tune_rbf$best.performance
## cost
## 8 0.5623413
## [1] 0.16875
svm_rbf_best <- svm(Purchase ~ ., data = OJ_train, kernel = "radial",
cost = tune_rbf$best.parameters$cost)
test_err_rbf2 <- mean(predict(svm_rbf_best, OJ_test) != OJ_test$Purchase)
test_err_rbf2
## [1] 0.1777778
# (g) 多項式カーネル(degree=2)
svm_poly <- svm(Purchase ~ ., data = OJ_train, kernel = "polynomial", degree = 2)
train_err_poly <- mean(predict(svm_poly, OJ_train) != OJ_train$Purchase)
test_err_poly <- mean(predict(svm_poly, OJ_test) != OJ_test$Purchase)
train_err_poly; test_err_poly
## [1] 0.1825
## [1] 0.2222222
# 多項式カーネルのコスト調整
set.seed(1)
tune_poly <- tune(svm, Purchase ~ ., data = OJ_train, kernel = "polynomial", degree = 2,
ranges = list(cost = 10^seq(-2, 1, by = 0.25)))
tune_poly$best.parameters; tune_poly$best.performance
## cost
## 11 3.162278
## [1] 0.1775
解答:
訓練集合800件、テスト集合270件に分割した。
コスト0.01の線形SVMを訓練したところ、サポートベクター数432(CHクラス217, MMクラス215)であった。訓練誤分類率は約16.9%、テスト誤分類率は約**17.8%**である。混同行列は以下の通り。
Predicted
Actual CH MM
CH 439 53
MM 82 226
Predicted
Actual CH MM
CH 142 19
MM 29 80
summary(svm_lin)
から、サポートベクター432個の内訳はCH側217個、MM側215個と出力された。コスト=0.01、カーネル=linear、γ=0.0556(γは線形カーネルでは特徴数の逆数に相当)も表示された。コストが非常に小さいためマージンが広く取られ、多数の点がサポートベクターになっていると読み取れる。また、線形カーネルゆえに決定境界は線形超平面(この場合217次元空間での線形境界)である。
コストをCVで調整した結果、最適コスト$$(厳密には$10^{-0.5}$)となった。このときCV推定誤差は16.87%である。最適コストで再学習したモデルの訓練誤分類率は16.0%、テスト誤分類率は**18.15%**となった。初期モデル(cost=0.01)と比べ訓練誤差はわずかに改善したが、テスト誤差はほぼ同程度である。むしろ若干悪化しており(17.8% → 18.2%)、これは最適コストでモデルがやや複雑になり過学習気味になった可能性がある。総じて、コスト調整による大きな性能差は見られなかった。
RBFカーネルSVM(デフォルトcost=1)では、訓練誤分類率14.75%、テスト誤分類率15.56%と線形モデルより改善した。サポートベクター数は367個(CH:184, MM:183)で、線形モデルの432個から減少した。これはRBFカーネルが非線形の柔軟な境界を描けるために、線形ではマージン上だった点の一部が境界から離れて誤分類を避けられ、サポートベクターの必要数が減ったと考えられる。モデルの柔軟性が上がったことで訓練誤差・テスト誤差とも線形より低くなっており、非線形境界が有効なことが示唆される。
RBFカーネルについてコストを調整した結果、最適コスト$$(CV誤差16.50%)となった。このモデルのテスト誤分類率は**15.93%**であり、調整前の15.56%からほぼ変化しなかった。訓練誤差も14.62%で調整前14.75%とほぼ同様。つまり、コストをやや下げても性能は大きく変わらず、元々デフォルト設定でほぼ最適に近かったと考えられる。なお、最適コストが1未満だったのは、多少マージンを広げた方がCV性能が良かった(過学習傾向を抑えた)ためと思われる。
多項式カーネル(2次)では、訓練誤分類率17.13%、テスト誤分類率18.15%と線形モデルとほぼ同程度であった。サポートベクター数は452個と線形(432)より増えており、境界の柔軟性向上でマージン幅が狭まり多くの点が境界付近となったことが示唆される。CVによるコスト調整ではcost≈5.623が最適だったが、CV誤差は約17.13%で線形と大差なかった(むしろ僅かに悪い)。従って、多項式2次カーネルはこのデータでは有効な非線形性を提供できず、不必要にモデル複雑度を上げただけと考えられる。実際、テスト誤差も線形と同程度で、RBFのような改善は見られなかった。総合すると、RBFカーネルSVMが最も良好(テスト誤差約15.6%)で、線形と多項式2次は劣る結果であった。これは境界の形状が線形よりは非線形だが単純なものでは表現しきれず、RBFのような柔軟な曲線が必要だったことを示唆する。一方でRBFほど高次の非線形は不要だったため、多項式2次では十分改善できなかったとも解釈できる。モデル選択としては、RBFカーネルを用いたSVMがこの問題には適合していると結論付けられる。