モモノキ&ナノネと一緒にR言語でOne class SVMを使った外れ値検出をやってみよう
ksvmのOne class SVMを使った外れ値検出の実践
ナノネ、今回はR言語でOne classSVM使った外れ値検出をやってみよう。
One class SVMって、普通のSVMと何が違うの?
One class SVMはSVMを応用した手法で、教師なしの1クラス分類だよ。正常データとして1つのクラスで学習を行って識別境界を決めるんだ。境界を越えるデータは外れ値として検出することができるよ。
SVMは教師あり学習だけど、One class SVMは教師なし学習なんだ。
異常検知を行いたいけど、普段は異常がほとんど発生せず異常データが集まらいケースも多いよね。そんな場合にも有効な外れ検知手法みたいだよ。
原理的はカーネルを使って高次元の特徴空間にデータを写像。正常データは原点から離れた位置に分布されるけど、外れ値は原点付近に集まるように配置される。それによって正常か異常かを区別できるみたいだよ。
SVMの原理も正確に分かってないから、ちょっと難しいなぁ。詳しい内容は後で調べて勉強するよ。
それでは実際にOne class SVMを試してみよう。でも、その前にテストデータを準備しないとね。
データはなんでもいいんだけど、グラフが簡単に描きやすいように今回は2次元データにしておこう。ナノネ、適当な数値データを作ってみて。
今回も乱数でいい?
set.seed(0)
x1 <- c(rnorm(300, mean=0, sd=1), runif(10, min=-5, max=5))
x2 <- c(rnorm(300, mean=0, sd=1), runif(10, min=-5, max=5))
plot(x1, x2)
こんなんで、いいかなぁ?乱数は正規分布をメインに、明確な外れ値用を入れるのに少しだけ一様分布も混ぜてみた。
とりあえずOK。作ったテストデータを使ってOne class SVMを実践してみよう。
学習に使うデータは分類クラスも用意しておく必要があるんだけど、1クラスだけだから全部同じ値(1)を書き込んでおけばいいみたいだよ。学習用データはデータフレーム型してまとめておこう。
df <- data.frame(type=1, x1, x2) # type:分類クラス(全て1を代入)
print(head(df))
print(tail(df))
学習用データ準備OK。
続いて学習に使うパッケージのインポート。今回はkernlabパッケージのksvmを使うよ。
# install.packages('kernlab')
library('kernlab')
次はいよいよksvmモデルを使って学習。One class SVMを使うときは、引数typeにone-svcを指定してね。他にもいくつか条件設定用のパラメータ値を決める必要があるんだけど、まずは下の例で実行してみて。
model <- ksvm(
type~.,
data=df,
type='one-svc',
kpar=list(sigma=0.001),
nu=0.02)
model
なんか?学習できたみたいだけど。Training error 0.025806とか表示されているけど、外れ値を検出した?
外れ値を検出したみたいだよ。predict関数を使って予測結果を取得してみよう。
pred <- predict(model, df)
str(pred)
predictの識別結果はTRUE/FALSEで返ってくるんだけど、TRUEが正常、FALSEが異常(識別境界を越えた外れ値)に対応しているよ。
table(pred)
8個を外れ値として検出したんだ。
8 / (8+302) # -> Training error 0.025806
なるほど、FALSE(外れ値)の比率が、Training errorの値と一致するんだ。
どのデータが外れ値として検出されたか、グラフで確認してみよう。
plot(x1, x2,
pch=21,
bg=c('red', 'green')[as.numeric(pred)+1], # TRUE(緑)/FALSE(赤)を数値に変換して色分け
xlim=c(-5, 5),
ylim=c(-5, 5))
緑マーカーは正常で、赤マーカーが外れ値だ。外れ値はデータの中心から離れた位置に8個表示されている。
グラフに境界線があると、もっと分かりやすいかも。
それなら以前練習で使ったコードが使えそうだよ。
コピぺしてチョット修正すれば識別境界が描けそう。やってみる。
library('kernlab')
# テストデータ作成
set.seed(0)
x1 <- c(rnorm(300, mean=0, sd=1), runif(10, min=-5, max=5))
x2 <- c(rnorm(300, mean=0, sd=1), runif(10, min=-5, max=5))
df <- data.frame(type=1, x1, x2) # type:分類クラス(全て1を代入)
# 学習
model <- ksvm(
type~.,
data=df,
type='one-svc',
kpar=list(sigma=0.001),
nu=0.02)
# 予測(学習データ)
pred <- predict(model, df)
# グラフの軸リミット
x1_min <- -5
x1_max <- 5
x2_min <- -5
x2_max <- 5
# グラフのフレームだけ描画
plot(x1, x2,
pch=21,
bg=c('blue', 'red')[as.numeric(pred)+1],
xlim=c(x1_min, x1_max),
ylim=c(x2_min, x2_max),
type='n')
# 境界プロット用のメッシュ作成
px <- seq(x1_min, x1_max, 0.02)
py <- seq(x2_min, x2_max, 0.02)
pgrid <- expand.grid(px, py)
names(pgrid) <- c("x1", "x2")
# メッシュデータで予測
pred.pgrid <- predict(model, pgrid)
# メッシュデータの予測結果で塗り潰し
my.colors <- c("#FFCCCC","#CCFFCC")
image(px, py, array(as.numeric(pred.pgrid), dim=c(length(px), length(py))),
xlim=c(x1_min, x1_max), ylim=c(x2_min, x2_max), add=T, col = my.colors)
# マーカープロット
points(x1, x2,
cex = 1.5,
pch = 21,
bg = c(2, 3)[as.numeric(pred)+1])
# メッシュデータの予測結果で等高線プロット
contour(px, py, array(as.numeric(pred.pgrid), dim=c(length(px), length(py))),
xlim=c(x1_min, x1_max), ylim=c(x2_min, x2_max),
col="orange", lwd=2, drawlabels=F, add=T)
識別境界も表示できた!
次は識別結果に影響するパラメータを変更してみよう。主に使うパラメーターは二つ(nu、sigma)あるんだけど、まずnuから。
nuは外れ値として許容する上限比率的なもので、one class SVM特有のパラメータだよ。nuを大きくとると外れ値が増えて、nuを小さくすると外れ値が減る方向に変化するよ。
実際にパラメータを変更して結果を比較した方が分かりやすいかも。nuを4パターン変更したときのモデルを作って、識別境界を可視化してみるね。
library('kernlab')
# テストデータ作成
set.seed(0)
x1 <- c(rnorm(300, mean=0, sd=1), runif(10, min=-5, max=5))
x2 <- c(rnorm(300, mean=0, sd=1), runif(10, min=-5, max=5))
df <- data.frame(type=1, x1, x2) # type:分類クラス(全て1を代入)
# グラフの軸リミット
x1_min <- -5
x1_max <- 5
x2_min <- -5
x2_max <- 5
par(pty='s')
par(mfrow=c(2, 2))
# 4パターンのnu値で識別結果を比較(学習&グラフ描画)
nu_try <- c(0.5, 0.2, 0.05, 0.02)
for(nu in nu_try){
# 学習
model <- ksvm(
type~.,
data=df,
type='one-svc',
kpar=list(sigma=0.001),
nu=nu)
# 予測(学習データ)
pred <- predict(model, df)
# 境界プロット用のメッシュ作成
px <- seq(x1_min, x1_max, 0.05)
py <- seq(x2_min, x2_max, 0.05)
pgrid <- expand.grid(px, py)
names(pgrid) <- c("x1", "x2")
# メッシュデータで予測
pred.pgrid <- predict(model, pgrid)
# グラフのフレームだけ描画
plot(x1, x2,
main = paste0("nu=", nu),
pch=21,
bg=c('blue', 'red')[as.numeric(pred)+1],
xlim=c(x1_min, x1_max),
ylim=c(x2_min, x2_max),
type='n')
# メッシュデータの予測結果で塗り潰し
my.colors <- c("#FFCCCC","#CCFFCC")
image(px, py, array(as.numeric(pred.pgrid), dim=c(length(px), length(py))),
xlim=c(x1_min, x1_max), ylim=c(x2_min, x2_max), add=T, col = my.colors)
# マーカープロット
points(x1, x2,
cex = 1.0,
pch = 21,
bg = c(2, 3)[as.numeric(pred)+1])
# メッシュデータの予測結果で等高線プロット
contour(px, py, array(as.numeric(pred.pgrid), dim=c(length(px), length(py))),
xlim=c(x1_min, x1_max), ylim=c(x2_min, x2_max),
col="orange", lwd=1, drawlabels=F, add=T)
}
par(mfrow=c(1, 1))
本当だ、nuの設定値が大きいと、外れ値(赤)として検出するデータ数も多くなってる。
次はsigmaだよ。sigmaはカーネルのパラメータなんだけど、変更すると境界の複雑さが変わるよ。sigmaに大きな値を指定すると複雑な境界を作れるんだ。でもあまり大きな値を設定すると過学習傾向になって、境界がグニャグニャになったり、離れ小島が沢山できたりするので注意してね。
sigmaも4パターンのモデルを作って、識別境界の違いを可視化してみよう。
library('kernlab')
# テストデータ作成
set.seed(0)
x1 <- c(rnorm(300, mean=0, sd=1), runif(10, min=-5, max=5))
x2 <- c(rnorm(300, mean=0, sd=1), runif(10, min=-5, max=5))
df <- data.frame(type=1, x1, x2) # type:分類クラス(全て1を代入)
# グラフの軸リミット
x1_min <- -5
x1_max <- 5
x2_min <- -5
x2_max <- 5
par(pty='s')
par(mfrow=c(2, 2))
# 4パターンのsigma値で識別結果を比較(学習&グラフ描画)
sigma_try <- c(1, 0.5, 0.1, 0.01)
for(sigma in sigma_try){
# 学習
model <- ksvm(
type~.,
data=df,
type='one-svc',
kpar=list(sigma=sigma),
nu=0.05)
# 予測(学習データ)
pred <- predict(model, df)
# 境界プロット用のメッシュ作成
px <- seq(x1_min, x1_max, 0.05)
py <- seq(x2_min, x2_max, 0.05)
pgrid <- expand.grid(px, py)
names(pgrid) <- c("x1", "x2")
# メッシュデータで予測
pred.pgrid <- predict(model, pgrid)
# グラフのフレームだけ描画
plot(x1, x2,
main = paste0("sigma=", sigma, " (nu=0.05)"),
pch=21,
bg=c('blue', 'red')[as.numeric(pred)+1],
xlim=c(x1_min, x1_max),
ylim=c(x2_min, x2_max),
type='n')
# メッシュデータの予測結果で塗り潰し
my.colors <- c("#FFCCCC","#CCFFCC")
image(px, py, array(as.numeric(pred.pgrid), dim=c(length(px), length(py))),
xlim=c(x1_min, x1_max), ylim=c(x2_min, x2_max), add=T, col = my.colors)
# マーカープロット
points(x1, x2,
cex = 1.0,
pch = 21,
bg = c(2, 3)[as.numeric(pred)+1])
# メッシュデータの予測結果で等高線プロット
contour(px, py, array(as.numeric(pred.pgrid), dim=c(length(px), length(py))),
xlim=c(x1_min, x1_max), ylim=c(x2_min, x2_max),
col="orange", lwd=1, drawlabels=F, add=T)
}
par(mfrow=c(1, 1))
たしかに、sigmaの設定が大きいと境界線がグニャグニャだ。目的に合った適正な数値を選ぶ必要がありそう。
今回のOne class SVMを使った外れ値検出の練習は以上で。
またね!
お疲れ様でした。またね!
0 件のコメント :
コメントを投稿