Go言語の機械学習パッケージで異常検知する方法

# はじめに

Go言語の機械学習パッケージを使って異常検知する方法の紹介です。数値データーの配列から異常値（外れ値）を見つける２つのアルゴリズムを実装したパッケージの使い方を紹介します。


# Local Outlier Factor(LOF)

日本語では「局所外れ値因子法」というらしいです。

https://ja.wikipedia.org/wiki/%E5%B1%80%E6%89%80%E5%A4%96%E3%82%8C%E5%80%A4%E5%9B%A0%E5%AD%90%E6%B3%95


### LOFのGo言語実装

https://github.com/zavgorodnii/golof


メンテナンスされていないようなのとログの出力が多いのでパッケージとして使いやすようにForkしたのが

https://github.com/twsnmp/golof

です。

### サンプルプログラム

```go

package main

import (
	"image/color"
	"log"

	"github.com/twsnmp/golof/lof"
	"gonum.org/v1/plot"
	"gonum.org/v1/plot/plotter"
	"gonum.org/v1/plot/vg"
)

func main() {
	// テストデータ
	points := [][]float64{
		{-4.8447532242074978, -5.6869538132901658},
		{1.7265577109364076, -2.5446963280374302},
		{-1.9885982441038819, 1.705719643962865},
		{-1.999050026772494, -4.0367551415711844},
		{-2.0550860126898964, -3.6247409893236426},
		{-1.4456945632547327, -3.7669258809535102},
		{-4.6676062022635554, 1.4925324371089148},
		{-3.6526420667796877, -3.5582661345085662},
		{6.4551493172954029, -0.45434966683144573},
		{-0.56730591589443669, -5.5859532963153349},
		{-5.1400897823762239, -1.3359248994019064},
		{5.2586932439960243, 0.032431285797532586},
		{6.3610915734502838, -0.99059648246991894},
		{-0.31086913190231447, -2.8352818694180644},
		{1.2288582719783967, -1.1362795178325829},
		{-0.17986204466346614, -0.32813130288006365},
		{2.2532002509929216, -0.5142311840491649},
		{-0.75397166138399296, 2.2465141276038754},
		{1.9382517648161239, -1.7276112460593251},
		{1.6809250808549676, -2.3433636210337503},
		{0.68466572523884783, 1.4374914487477481},
		{2.0032364431791514, -2.9191062023123635},
		{-1.7565895138024741, 0.96995712544043267},
		{3.3809644295064505, 6.7497121359292684},
		{-4.2764152718650896, 5.6551328734397766},
		{-3.6347215445083019, -0.85149861984875741},
		{-5.6249411288060385, -3.9251965527768755},
		{4.6033708001912093, 1.3375110154658127},
		{-0.685421751407983, -0.73115552984211407},
		{-2.3744241805625044, 1.3443896265777866},
	}
	log.Println("start")
	// LOFの計算
	samples := lof.GetSamplesFromFloat64s(points)
	lofGetter := lof.NewLOF(5)
	lofGetter.Train(samples)
	mapping := lofGetter.GetLOFs(samples, "fast")
	// グラフの作成
	pts1 := plotter.XYs{}
	pts2 := plotter.XYs{}
	for sample, factor := range mapping {
		point := sample.GetPoint()
		if factor > 1.5 {
			pts1 = append(pts1, plotter.XY{X: point[0], Y: point[1]})
			log.Printf("anomaly: %v,  \tLOF: %f\n", point, factor)
		} else {
			pts2 = append(pts2, plotter.XY{X: point[0], Y: point[1]})
		}
	}
	lofGetter.Reset()
	p := plot.New()
	p.Title.Text = "golof Example"
	p.X.Label.Text = "X"
	p.Y.Label.Text = "Y"
	p.Add(plotter.NewGrid())
	s1, err := plotter.NewScatter(pts1)
	if err != nil {
		log.Fatal(err)
	}
	s2, err := plotter.NewScatter(pts2)
	if err != nil {
		log.Fatal(err)
	}
	p.Add(s1, s2)
	p.Legend.Add("anomaly", s1)
	p.Legend.Add("normal", s2)
	s1.GlyphStyle.Color = color.RGBA{R: 255, B: 128, A: 255}
	s2.GlyphStyle.Color = color.RGBA{R: 0, B: 255, A: 255}
	if err := p.Save(4*vg.Inch, 4*vg.Inch, "points.png"); err != nil {
		log.Fatal(err)
	}
	log.Println("end")
}

```

### 実行結果

```terminal
$go run main.go
2024/11/02 05:01:57 start
2024/11/02 05:01:57 anomaly: [-4.27641527186509 5.6551328734397766],  	LOF: 1.768054
2024/11/02 05:01:57 anomaly: [3.3809644295064505 6.749712135929268],  	LOF: 1.932724
2024/11/02 05:01:57 end
```

### 出力されたグラフ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3874862/450e8461-cbd1-f81c-adf9-4e61c75235ca.png)


赤い点が異常値です。外れている感じがわかります。

# Isolation Forest

https://qiita.com/tchih11/items/d76a106e742eb8d92fb4

### Isolation ForestのGo言語実装

https://github.com/bn4t/go-iforest

### サンプルコード

GitHUBのリポジトリにあるサンプルは不完全なので動作しません。
LOFのサンプルを移植してみました。

```go
package main

import (
	"image/color"
	"log"

	go_iforest "github.com/codegaudi/go-iforest"
	"gonum.org/v1/plot"
	"gonum.org/v1/plot/plotter"
	"gonum.org/v1/plot/vg"
)

func main() {
	// テストデータ
	points := [][]float64{
		{-4.8447532242074978, -5.6869538132901658},
		{1.7265577109364076, -2.5446963280374302},
		{-1.9885982441038819, 1.705719643962865},
		{-1.999050026772494, -4.0367551415711844},
		{-2.0550860126898964, -3.6247409893236426},
		{-1.4456945632547327, -3.7669258809535102},
		{-4.6676062022635554, 1.4925324371089148},
		{-3.6526420667796877, -3.5582661345085662},
		{6.4551493172954029, -0.45434966683144573},
		{-0.56730591589443669, -5.5859532963153349},
		{-5.1400897823762239, -1.3359248994019064},
		{5.2586932439960243, 0.032431285797532586},
		{6.3610915734502838, -0.99059648246991894},
		{-0.31086913190231447, -2.8352818694180644},
		{1.2288582719783967, -1.1362795178325829},
		{-0.17986204466346614, -0.32813130288006365},
		{2.2532002509929216, -0.5142311840491649},
		{-0.75397166138399296, 2.2465141276038754},
		{1.9382517648161239, -1.7276112460593251},
		{1.6809250808549676, -2.3433636210337503},
		{0.68466572523884783, 1.4374914487477481},
		{2.0032364431791514, -2.9191062023123635},
		{-1.7565895138024741, 0.96995712544043267},
		{3.3809644295064505, 6.7497121359292684},
		{-4.2764152718650896, 5.6551328734397766},
		{-3.6347215445083019, -0.85149861984875741},
		{-5.6249411288060385, -3.9251965527768755},
		{4.6033708001912093, 1.3375110154658127},
		{-0.685421751407983, -0.73115552984211407},
		{-2.3744241805625044, 1.3443896265777866},
	}
	log.Println("start")
	/// Isolation Forestの計算
	f, err := go_iforest.NewIForest(points, 10000, 20)
	if err != nil {
		log.Fatal(err)
	}
	// グラフの作成
	ptsA := plotter.XYs{}
	ptsN := plotter.XYs{}
	for i := 0; i < len(points); i++ {
		score := f.CalculateAnomalyScore(points[i])
		if score > 0.6 {
			log.Printf("anomaly: %v, \tScore: %f", points[i], score)
			ptsA = append(ptsA, plotter.XY{X: points[i][0], Y: points[i][1]})
		} else {
			ptsN = append(ptsN, plotter.XY{X: points[i][0], Y: points[i][1]})
		}
	}
	p := plot.New()
	p.Title.Text = "Isolation Forest Example"
	p.X.Label.Text = "X"
	p.Y.Label.Text = "Y"
	p.Add(plotter.NewGrid())
	s1, err := plotter.NewScatter(ptsA)
	if err != nil {
		log.Fatal(err)
	}
	s2, err := plotter.NewScatter(ptsN)
	if err != nil {
		log.Fatal(err)
	}
	p.Add(s1, s2)
	p.Legend.Add("anomaly", s1)
	p.Legend.Add("normal", s2)
	s1.GlyphStyle.Color = color.RGBA{R: 255, B: 128, A: 255}
	s2.GlyphStyle.Color = color.RGBA{R: 0, B: 255, A: 255}
	if err := p.Save(4*vg.Inch, 4*vg.Inch, "points.png"); err != nil {
		log.Fatal(err)
	}
	log.Println("end")
}
```

### 実行結果

```terminal
$go run main.go
2024/11/02 05:01:57 start
2024/11/02 05:01:57 Anomary: [-4.27641527186509 5.6551328734397766],  	LOF: 1.768054
2024/11/02 05:01:57 Anomary: [3.3809644295064505 6.749712135929268],  	LOF: 1.932724
2024/11/02 05:01:57 end

```

判定するスコアのしきい値を調整すれば、同じ結果になりました。


### グラフ出力

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3874862/46ebf876-0e28-098f-7965-20a15bfa23e1.png)


# 余談

この２つの異常検知のパッケージは、TWSNMPシリーズで使っています。

https://note.com/twsnmp/n/na5c62f59833d

ポーリングの結果の数値から異常を検知するAI分析という名前にしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3874862/84d144d0-a2e2-5af6-80e0-603a7e78325c.png)


TWSNMP　FCでは両方のアルゴリズムに対応しています。（最初は、LOFだけだったのですが、Isolation Forestを知ってから追加しました。
切り替えできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3874862/42c2d8b5-4c67-2f56-91ff-340458fbc885.png)


:::note alert
同じデータでテストするとLOFはIsolation Forestの40倍もメモリーを使用しました。
Isolation Forestを使ったほうがよいと思います。
:::