外れ値検知手法のベンチマークを測定するスクリプトをPyODとOptunaで書いた

はじめに

外れ値検知の代表的な手法であるkNN, LOF, OC-SVM, Isolation Forestについて、ベンチマークを測定するスクリプトを作成した。 各手法は外れ値検知ライブラリのPyODに実装されており、今回はOptunaによるハイパーパラメータ探索も入れている。

PyODのベンチマークについては公式にもドキュメントが用意されているので、一読を勧める。 pyod.readthedocs.io

準備

PyODはpipでインストール可能である。

pip install pyod

また、以下のgithubレポジトリからベンチマーク用のデータをダウンロードしておく。 github.com

なおベンチマーク用データ自体は以下のサイトから入手可能である。種類はたくさんあるが、今回はそこから16種類のデータセットを利用する。 odds.cs.stonybrook.edu

スクリプトたち

ベンチマークスクリプトと同じ階層に dataディレクトリを作成しておき、ベンチマーク用データはdataディレクトリ以下にまとめて置いておくこと。

スクリプトはPyLint的に10.00(満点)になるように作ったが、可読性はいまいちかもしれない。

trial数やハイパーパラメータの探索範囲などはかなり適当に決めている。

おわりに

PyOD使おうぜ!(n回目)

Optunaもいいぞ!

Rapid Distance-Based Outlier Detection via Sampling (NIPS 2013) に基づく外れ値検知手法をPyODフォーマットで実装した

はじめに

"Rapid Distance-Based Outlier Detection via Sampling" という論文の中で提案された手法を、外れ値検知のためのPythonライブラリ PyOD のフォーマットに落とし込んで、手軽に使えるように実装したということ。併せてベンチマークデータを用いて性能を評価した。

論文

論文は以下から読むことができる。

Sugiyama, M., Borgwardt, K. M., "Rapid Distance-Based Outlier Detection via Sampling," Advances in Neural Information Processing Systems (NIPS 2013), 467-475, 2013.

論文著者のSugiyama氏による解説資料(PDF)はこちらから。

Sugiyama氏によるR および C 言語による実装例はこちらから。

手法の概要

データセット全体から一度だけサンプリングしてサブセットを作り、保持する(再サンプリングは行わない)。 新規入力データの異常スコアは、サブセット上の最近傍サンプルとの距離を求めることで計算される。

サブセットのサイズを十分に小さくしても、 データセット全体を用いる他の外れ値検知手法と匹敵する検知精度を得ることができる。 かつ計算時間を大きく減らせるのでハッピー。

実装したコード

PyODのBaseDetectorクラスを継承し、Samplingクラスを実装した。

コードを表示する gist.github.com

引数の意味は以下の通りである。

引数 説明
subset_size サンプリングするサブセットのサイズを指定する。int型ならばサブセットのサンプル数、float型ならば全体に対するサブセットの比率
metric 距離を指定する。sklearn準拠。
metric_params metricのキーワード引数
random_state 乱数の種 (int) or RandomState インスタンス
contamination 外れ値の(想定)比率

デモンストレーション

準備

PyODはpipコマンドでインストールできる。

pip install pyod

上記リンクにあるコードをsampling.pyなど適当な名前で保存する。

ノートブックその1

実行例を以下のnotebookに示す。サブセットのサイズは20にした。

gist.github.com

ノートブックその2

PyOD付属のベンチマーク用のスクリプトを少し改変する。件の論文にも比較手法として掲載されている外れ値検知の各手法と比較する。

  • Angle-based Outlier Detector (ABOD)
  • k-Nearest Neighbors Detector (kNN); k = 5
  • Isolation Forest (IForest)
  • Local Outlier Factor (LOF); k=10
  • One-Class SVM (OCSVM)
  • Sampling (今回の手法)

データセットの種類は16種類である。サンプリングにおけるサブセットのサイズは50で固定した(デモンストレーションなので50という数字に特に意味はない)。 ベンチマークデータは以下のgithubリポジトリからダウンロードして使う。

https://github.com/yzhao062/pyod/tree/master/notebooks

gist.github.com

"Sampling"の実行時間はとても短いが、外れ値検知の性能は他の手法と匹敵していることが分かる。

おわりに

本記事では"Rapid Distance-Based Outlier Detection via Sampling"において提案された外れ値検知手法を実装し、その検知性能を評価した。

学習データ全体からサブセットを一度だけサンプリングして保持し、以降はサブセット上で最近傍距離を求めるというシンプルな手法である。 PyOD付属のベンチマークデータを用いた外れ値検知実験の結果から、サンプリングにより検知精度を大きく落とすことなく、実行時間を大幅に減らせることを確かめた。

学習データ量が十分にあり、かつ外れ値がある程度含まれている場合には有望な手法のひとつだろう。何より実装が簡単である。学習データに外れ値がまったく含まれていない場合にも、ある程度は動くのではと考える(実験していないので推測)。

selected.elの設定 2022

はじめに

以前、selected.elの設定を記事にしたことがあった。

tam5917.hatenablog.com

今回はその設定を一部見直し、新しい関数に置き換えた部分があるので、それをまとめておこうというのが主旨である。 selected.el自体は以下の記事を読むのがよい。

qiita.com

設定

以下にコードを示す。クリックで展開する。requireされているパッケージは適当に探せばすぐに見つかるものばかりである。

コードを表示する

;; http://github.com/Kungsgeten/selected.el
(require 'selected)

;; https://github.com/minad/consult
(require 'consult)

;; https://github.com/emacsorphanage/anzu
(require 'anzu)

;; https://github.com/victorhge/iedit
(require 'iedit)

;; https://github.com/magnars/multiple-cursors.el
(require 'multiple-cursors)

;; https://github.com/xuchunyang/osx-dictionary.el
(require 'osx-dictionary)

;; http://www.emacswiki.org/emacs/download/replace-from-region.el
(require 'replace-from-region)

;; http://github.com/Malabarba/emacs-google-this
(require 'google-this)

;; https://github.com/remyferre/comment-dwim-2
(require 'comment-dwim-2)

;; https://gist.github.com/tam17aki/be6445e3b2b28a70ccce3546d2963680
(require 'consult-selected)

;; https://gist.github.com/tam17aki/f5046e9381436e783df385b42b11125b
(require 'consult-thing-at-point)

;; 参考 http://blog.fujimisakari.com/elisp_useful_for_programming/
(defun region-to-single-quote ()
  (interactive)
  (quote-formater "'%s'" "^\\(\"\\).*" ".*\\(\"\\)$"))
(defun region-to-double-quote ()
  (interactive)
  (quote-formater "\"%s\"" "^\\('\\).*" ".*\\('\\)$"))
(defun region-to-bracket ()
  (interactive)
  (quote-formater "\(%s\)" "^\\(\\[\\).*" ".*\\(\\]\\)$"))
(defun region-to-square-bracket ()
  (interactive)
  (quote-formater "\[%s\]" "^\\(\(\\).*" ".*\\(\)\\)$"))
(defun region-to-brace ()
  (interactive)
  (quote-formater "\%s\]" "^\\(\(\\).*" ".*\\(\)\\)$"))
(defun quote-formater (quote-format re-prefix re-suffix)
  (if mark-active
      (let* ((region-text (buffer-substring-no-properties
                           (region-beginning) (region-end)))
             (replace-func
              (lambda (re target-text)
                (replace-regexp-in-string re "" target-text nil nil 1)))
             (text (funcall replace-func re-suffix
                            (funcall replace-func re-prefix region-text))))
        (delete-region (region-beginning) (region-end))
        (insert (format quote-format text)))
    (error "Not Region selection")))

(defun my:google-this ()
  "検索確認をスキップして直接検索実行"
  (interactive)
  (google-this (current-word) t))

(setq google-this-location-suffix "co.jp")

(when (require 'selected nil t)

  ;; コメントアウト・アンコメントアウト
  (define-key selected-keymap (kbd ";") #'comment-dwim-2)

  ;; 選択した関数のヘルプを表示
  (define-key selected-keymap (kbd "f") #'describe-function)

  ;; 選択した変数のヘルプを表示
  (define-key selected-keymap (kbd "v") #'describe-variable)

  ;; 選択したシンボルのヘルプを表示
  (define-key selected-keymap (kbd "y") #'describe-symbol)

  ;; 辞書を引く (Mac限定)
  (define-key selected-keymap (kbd "d") #'osx-dictionary-search-pointer)

  ;; 置換関連
  (define-key selected-keymap (kbd "q") #'query-replace-from-region)
  (define-key selected-keymap (kbd "Q") #'anzu-query-replace)

  ;; リージョンの文字数や単語数をカウント
  (define-key selected-keymap (kbd "=") #'count-words-region)

  ;; 複数カーソルによるマーク
  (define-key selected-keymap (kbd "A") #'mc/mark-all-like-this)
  (define-key selected-keymap (kbd "n") #'mc/mark-next-like-this)
  (define-key selected-keymap (kbd "p") #'mc/mark-previous-like-this)
  (define-key selected-keymap (kbd "u") #'mc/unmark-next-like-this)
  (define-key selected-keymap (kbd "U") #'mc/unmark-previous-like-this)
  (define-key selected-keymap (kbd "s") #'mc/skip-to-next-like-this)
  (define-key selected-keymap (kbd "S") #'mc/skip-to-previous-like-this)

  ;; 同時編集 iedit
  ;; デフォルトではバッファ全体にカーソルが分身して編集可能になるので、
  ;; ナローイングを適宜用いる
  (define-key selected-keymap (kbd "i") #'iedit-mode)

  ;; 参考 http://blog.fujimisakari.com/elisp_useful_for_programming/
  ;; リージョンをシングルクオートで囲う
  (define-key selected-keymap (kbd "\'") #'region-to-single-quote)

  ;; リージョンをダブルクオートで囲う
  (define-key selected-keymap (kbd "\"") #'region-to-double-quote)

  ;; リージョンをブラケット(カッコ)で囲う
  (define-key selected-keymap (kbd "(") #'region-to-bracket)

  ;; リージョンをカギカッコで囲う
  (define-key selected-keymap (kbd "[") #'region-to-square-bracket)

  ;; consult-line
  (define-key selected-keymap (kbd "w") #'consult-line-thing-at-point)

  ;; google 検索
  (define-key selected-keymap (kbd "g") #'my:google-this)

  ;; consultインターフェイスで選択
  (define-key selected-keymap (kbd "l") #'consult-selected)

  ;; 有効化
  (selected-global-mode 1))

主な変更点

  • counselの削除
  • swiperの削除
  • consultの導入(consult-apropos, consult-selected, consult-line-thing-at-point

consult-selectedconsult-line-thing-at-pointは新たに書いた関数なので、以下で詳しく説明する。

consult系の関数群の導入

takaxp氏により、helm-selected.elcounsel-selected.elが書かれていたので、これのconsult用の代替ライブラリとしてconsult-selected.elを書いた。

さらに、選択されたリージョンを元にconsult-lineを発動させるために、consult-thing-at-point.elを書いた。

このライブラリにより、consult-line-thing-at-pointという関数が提供されるので、これを適当なキーにバインドして使うことができる。

おわりに

selected.elは便利なので、利用を勧めたい。

consult-ripgrepの検索対象を現在開いているバッファたちに限定するには

以下の関数を使う(consult-ripgrep-multi-file)。通常のconsult-ripgrepはあるディレクトリ以下の全ファイルが検索対象になるが、今回は「現在開いているバッファ(に対応するファイルたち)」に検索対象を限定したということ(串刺し検索)。

consult-line-multihelm-swoopの代替に相当する。

検索対象から外したい*scratch*バッファや*Messages*バッファ の除外にはido-ignore-buffersで指定される正規表現を利用した。

;; perspective.el内の関数を参考にした
(defun consult--make-ignore-buffer-rx ()
  (defvar ido-ignore-buffers)
  (if ido-ignore-buffers
      (rx-to-string (append (list 'or)
                            (mapcar (lambda (rx) `(regexp ,rx))
                                    ido-ignore-buffers)))
    "$^"))

(defun consult--get-curret-buffer-list ()
  (let (buffer-mode-matches)
    (mapc (lambda (buf)
            (when (get-buffer buf)
              (with-current-buffer (get-buffer buf)
                (if (and (buffer-live-p buf)
                         (not (string-match-p (consult--make-ignore-buffer-rx)
                                              (buffer-name buf))))
                    (add-to-list 'buffer-mode-matches (buffer-file-name buf))))))
          (buffer-list))
    buffer-mode-matches))

(defun consult-ripgrep-multi-files ()
  "Call `consult-ripgrep' for the current buffers (multiple files)."
  (interactive)
  (let* ((consult-project-function (lambda (x) nil))
         (files (mapconcat
                 (lambda (x) (concat (shell-quote-argument x)))
                 (consult--get-curret-buffer-list) " "))
         (consult-ripgrep-args
          (concat "rg "
                  "--null "
                  "--line-buffered "
                  "--color=never "
                  "--line-number "
                  "--smart-case "
                  "--no-heading "
                  "--max-columns=1000 "
                  "--max-columns-preview "
                  "--with-filename "
                  files)))
    (consult-ripgrep)))

consult-ripgrepの検索対象をカレントバッファに限定するには

以下の関数を使う。 consult-lineと実質的な働きは変わらないのがメリット。 つまり、 consult-ripgrepの設定(migemo化など)が活きるので、例えばconsult-line系に限定したmigemo化の設定は不要となる点。 そのほか、consult-lineをmigemo化すると、最初の数文字の入力で表示がもたつく(おそらくmigemoに関する正規表現の展開がデカイ)ので、今回のconsult-ripgrep-single-fileによりそれを回避したスムーズな入力と候補絞り込みが実現できる。

(defun consult-ripgrep-single-file ()
    "Call `consult-ripgrep' for the current buffer (a single file)."
    (interactive)
    (let ((consult-project-function (lambda (x) nil))
          (consult-ripgrep-args
           (concat "rg "
                   "--null "
                   "--line-buffered "
                   "--color=never "
                   "--line-number "
                   "--smart-case "
                   "--no-heading "
                   "--max-columns=1000 "
                   "--max-columns-preview "
                   "--with-filename "
                   (shell-quote-argument buffer-file-name))))
      (consult-ripgrep)))

参考

github.com

カーソル下のシンボルを初期入力にしてconsult-ripgrepをするには

こうする。

(defun consult-ripgrep-symbol-at-point ()
  (interactive)
  (consult-ripgrep nil (thing-at-point 'symbol)))

C-uつきで呼び出したときにシンボル初期入力を行いたい場合は

(defun my-consult-ripgrep (use-symbol)
  (interactive "p")
  (cond ((eq use-symbol 1)
         (call-interactively 'consult-ripgrep))
        ((eq use-symbol 4)
         (call-interactively 'consult-ripgrep-symbol-at-point))))

とする。C-uなしで呼び出せば通常のconsult-ripgrepとなる。つまり

  • M-x my-consult-ripgrep ... 通常のconsult-ripgrep
  • C-u M-x my-consult-ripgrep ... カーソル下のシンボルでconsult-ripgrep

である。

consult-ripgrepのmigemo化

consult-ripgrepmigemo化に取り組んだ方がいらっしゃった。 www.yewton.net

しかしながら、上記の記事で紹介されている設定では手元の環境でうまく動かなかったので、少し修正してみたという話。

(require 'consult)
(defvar consult--migemo-regexp "")
(defun consult--migemo-regexp-compiler (input type ignore-case)
  (setq consult--migemo-regexp
        (mapcar #'migemo-get-pattern (consult--split-escaped input)))
  (cons (mapcar (lambda (x) (consult--convert-regexp x type))
                consult--migemo-regexp)
        (lambda (str)
          (consult--highlight-regexps consult--migemo-regexp str))))
(setq consult--regexp-compiler #'consult--migemo-regexp-compiler)

ひとまずこれで動くようになった。ただし以下のmigemo関連の設定を事前にしておくこと。

(require 'migemo)
(setq migemo-directory "/usr/share/cmigemo/utf-8")
(setq migemo-command (executable-find "cmigemo"))
(setq migemo-options '("-q" "--emacs" "--nonewline"))
(setq migemo-dictionary (expand-file-name "migemo-dict" migemo-directory))
(setq migemo-coding-system 'utf-8-unix) ;この指定が極めて重要
(setq migemo-user-dictionary nil)
(setq migemo-regex-dictionary nil)
(add-hook 'after-init-hook #'migemo-init)