モモノキ&ナノネと一緒に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 件のコメント :
コメントを投稿