Rでデータ解析を始めよう022 RでOne class SVM使った外れ値検出をやってみよう

Rでデータ解析を始めよう022 RでOne class SVM使った外れ値検出をやってみよう

統計ソフトR言語を使ったデータ解析の練習です。

モモノキ&ナノネと一緒に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次元データにしておこう。ナノネ、適当な数値データを作ってみて。

今回も乱数でいい?

In [1]:
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)を書き込んでおけばいいみたいだよ。学習用データはデータフレーム型してまとめておこう。

In [2]:
df <- data.frame(type=1, x1, x2) # type:分類クラス(全て1を代入)
In [3]:
print(head(df))
print(tail(df))
  type         x1          x2
1    1  1.2629543 -1.24455152
2    1 -0.3262334 -0.39570292
3    1  1.3297993  0.09739665
4    1  1.2724293 -0.23838695
5    1  0.4146414 -0.41182796
6    1 -1.5399500 -1.57721805
    type        x1         x2
305    1  2.498217 -2.1919993
306    1  4.756573 -0.8838232
307    1  4.747925  4.4342500
308    1 -1.493744  4.7734253
309    1 -1.060509 -4.6113341
310    1  4.509510  0.8772778

学習用データ準備OK。

続いて学習に使うパッケージのインポート。今回はkernlabパッケージのksvmを使うよ。

In [4]:
# install.packages('kernlab')
library('kernlab')

次はいよいよksvmモデルを使って学習。One class SVMを使うときは、引数typeにone-svcを指定してね。他にもいくつか条件設定用のパラメータ値を決める必要があるんだけど、まずは下の例で実行してみて。

In [5]:
model <- ksvm(
    type~.,
    data=df,
    type='one-svc',
    kpar=list(sigma=0.001),
    nu=0.02)
In [6]:
model
Support Vector Machine object of class "ksvm" 

SV type: one-svc  (novelty detection) 
 parameter : nu = 0.02 

Gaussian Radial Basis kernel function. 
 Hyperparameter : sigma =  0.001 

Number of Support Vectors : 8 

Objective Function Value : 18.4659 
Training error : 0.025806 

なんか?学習できたみたいだけど。Training error 0.025806とか表示されているけど、外れ値を検出した?

外れ値を検出したみたいだよ。predict関数を使って予測結果を取得してみよう。

In [7]:
pred <- predict(model, df)
In [8]:
str(pred)
 logi [1:310, 1] TRUE TRUE TRUE TRUE TRUE TRUE ...

predictの識別結果はTRUE/FALSEで返ってくるんだけど、TRUEが正常、FALSEが異常(識別境界を越えた外れ値)に対応しているよ。

In [9]:
table(pred)
pred
FALSE  TRUE 
    8   302 

8個を外れ値として検出したんだ。

In [10]:
8 / (8+302) # -> Training error 0.025806
0.0258064516129032

なるほど、FALSE(外れ値)の比率が、Training errorの値と一致するんだ。

どのデータが外れ値として検出されたか、グラフで確認してみよう。

In [11]:
plot(x1, x2, 
     pch=21,
     bg=c('red', 'green')[as.numeric(pred)+1], # TRUE(緑)/FALSE(赤)を数値に変換して色分け
     xlim=c(-5, 5),
     ylim=c(-5, 5))

緑マーカーは正常で、赤マーカーが外れ値だ。外れ値はデータの中心から離れた位置に8個表示されている。

グラフに境界線があると、もっと分かりやすいかも。

それなら以前練習で使ったコードが使えそうだよ。

コピぺしてチョット修正すれば識別境界が描けそう。やってみる。

In [12]:
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パターン変更したときのモデルを作って、識別境界を可視化してみるね。

In [13]:
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パターンのモデルを作って、識別境界の違いを可視化してみよう。

In [14]:
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 件のコメント :

コメントを投稿