ブログトップ 記事一覧 ログイン 無料ブログ開設

翡翠はコンピュータに卵を生むか

2017-02-13

CLMLのランダムフォレストを試してみる

ランダムフォレストは決定木ベースの分類/回帰モデルで、ニューラルネットやSVMなどと同様に非線形モデルなので線形分離不可能な問題にも使える。SVMはデータ数に対して指数的に計算時間がかかる一方、ランダムフォレストはデータ数をnとしてn*log(n)のオーダであり、軽い。また、SVMは基本的に二値分類器なので複数の学習器を組み合わせてマルチクラス分類することが多いが、ランダムフォレストは元からマルチクラスに対応していて追加のコストがかからない。さらに並列化も簡単にできるなど、扱いやすい性質をたくさん備えているので現実世界でよく使われている。

参考にしたランダムフォレストについての記事いろいろ

CLMLにランダムフォレストの実装があったので試してみる。

CLMLはCommon Lisp用の機械学習パッケージ集であり、Quicklispからインストールできる。ただし、あらかじめ処理系のdynamic-space-sizeを2560以上にして起動しておく必要があることに注意する。

先に必要ライブラリを読み込んでおく。

(ql:quickload :clml)
(ql:quickload :cl-online-learning)
(ql:quickload :alexandria)

データの読み込み

まずはデータの読み込みの例。サンプルデータをネットからダウンロードしてきて読み込む。

(defparameter *syobu*
  (clml.hjs.read-data:read-data-from-file 
   (clml.utility.data:fetch "https://mmaul.github.io/clml.data/sample/syobu.csv")
   :type :csv :csv-type-spec '(string integer integer integer integer)))

;; CL-USER> *syobu*
;; #<CLML.HJS.READ-DATA:UNSPECIALIZED-DATASET >
;; DIMENSIONS: 種類 | がく長 | がく幅 | 花びら長 | 花びら幅
;; TYPES:      UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN
;; NUMBER OF DIMENSIONS: 5
;; DATA POINTS: 150 POINTS

;; CL-USER> (aref (clml.hjs.read-data:dataset-points *syobu*) 0)
;; #("Setosa" 51 35 14 2)

ここから分かるように、データセットは特徴量の各次元の名前のリストと、データ点を表すベクタのベクタを持っている。例えば以下のようにして新たにデータセットを作れる。

(defparameter dataset1
  (clml.hjs.read-data:make-unspecialized-dataset
   '("class" "feat1" "feat2")
   (vector (vector "posi" 1 2)
           (vector "nega" -1 -2)
           (vector "posi" 10 20))))

データ点のベクタにラベルの文字列と数値が混在しているのが遅そう。

LIBSVMデータセットのデータを読み込む

今読んでるランダムフォレストのオンライン版の論文に出てくるものと同じデータセットで比較したいので、LIBSVMのデータセット集からmushroomsデータを読み込む。

cl-online-learningのread-data関数で読み込んでCLMLの形式に変換する。予測したい特徴の名前が後で必要になってくるので、クラスラベルに"class"と名前を付けて、残りの特徴名には単に連番を振っておく。

(defparameter *mushrooms-dim* 112)
(defparameter *mushrooms-train* (clol.utils:read-data "/home/wiz/datasets/mushrooms-train" *mushrooms-dim*))
(defparameter *mushrooms-test* (clol.utils:read-data "/home/wiz/datasets/mushrooms-test" *mushrooms-dim*))

(defun clol.datum->clml.datum (datum)
  (let ((label (if (> (car datum) 0) "posi" "nega")))
    (coerce (cons label (coerce (cdr datum) 'list)) 'vector)))

(defun clol.dataset->clml.dataset (dataset)
  (let ((datum-dim (length (cdar dataset))))
    (clml.hjs.read-data:make-unspecialized-dataset
     (cons "class" (mapcar #'(lambda (x) (format nil "~A" x)) (alexandria:iota datum-dim)))
     (map 'vector #'clol.datum->clml.datum dataset))))

(defparameter *mushrooms-train.clml* (clol.dataset->clml.dataset *mushrooms-train*))
(defparameter *mushrooms-test.clml* (clol.dataset->clml.dataset *mushrooms-test*))

ランダムフォレストの学習

lparallelを使って並列化しているので、まずカーネルサイズの設定をしておく。

(setf lparallel:*kernel* (lparallel:make-kernel 4))

上で作った *mushrooms-train.clml* データセットから学習する。予測対象のクラスラベルの特徴名をここで指定する。

(defparameter *forest* (clml.decision-tree.random-forest:make-random-forest *mushrooms-train.clml* "class"))
;; Evaluation took:
;;   81.367 seconds of real time
;;   269.523032 seconds of total run time (265.196590 user, 4.326442 system)
;;   [ Run times consist of 9.140 seconds GC time, and 260.384 seconds non-GC time. ]
;;   331.24% CPU
;;   276,010,481,282 processor cycles
;;   120,282,960,608 bytes consed

4コアCPUで計算しているが、かなり時間がかかってしまっている。

予測は、予測したいデータ点のラベル部分を "?" に置き換えたものを学習済みのモデルとともにpredict-forest関数に与える。例えば、テストセットの最初のデータ点を予測するには、

(aref (clml.hjs.read-data:dataset-points *mushrooms-test.clml*) 0)
;; #("nega" 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 1.0d0 1.0d0 0.0d0 0.0d0 0.0d0 0.0d0
;;   0.0d0 1.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 1.0d0 0.0d0 0.0d0
;;   1.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 1.0d0 1.0d0 0.0d0 0.0d0 1.0d0
;;   1.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0
;;   1.0d0 0.0d0 0.0d0 1.0d0 0.0d0 0.0d0 1.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0
;;   0.0d0 0.0d0 1.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 1.0d0 0.0d0
;;   0.0d0 1.0d0 0.0d0 0.0d0 1.0d0 0.0d0 0.0d0 1.0d0 0.0d0 0.0d0 0.0d0 1.0d0 0.0d0
;;   0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 1.0d0 0.0d0 0.0d0 0.0d0 0.0d0
;;   0.0d0 1.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 1.0d0 0.0d0 0.0d0)

(defparameter *query*
  #("?" 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 1.0d0 1.0d0 0.0d0 0.0d0 0.0d0 0.0d0
    0.0d0 1.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 1.0d0 0.0d0 0.0d0
    1.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 1.0d0 1.0d0 0.0d0 0.0d0 1.0d0
    1.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0
    1.0d0 0.0d0 0.0d0 1.0d0 0.0d0 0.0d0 1.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0
    0.0d0 0.0d0 1.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 1.0d0 0.0d0
    0.0d0 1.0d0 0.0d0 0.0d0 1.0d0 0.0d0 0.0d0 1.0d0 0.0d0 0.0d0 0.0d0 1.0d0 0.0d0
    0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 1.0d0 0.0d0 0.0d0 0.0d0 0.0d0
    0.0d0 1.0d0 0.0d0 0.0d0 0.0d0 0.0d0 0.0d0 1.0d0 0.0d0 0.0d0))

(clml.decision-tree.random-forest:predict-forest *query* *mushrooms-train.clml* *forest*)
; => "nega"

テストセットをまとめて予測するには、forest-validation関数を使う。

(clml.decision-tree.random-forest:forest-validation *mushrooms-test.clml* "class" *forest*)

;; Evaluation took:
;;   12.064 seconds of real time
;;   12.075773 seconds of total run time (12.044405 user, 0.031368 system)
;;   [ Run times consist of 0.337 seconds GC time, and 11.739 seconds non-GC time. ]
;;   100.10% CPU
;;   40,922,800,082 processor cycles
;;   10,874,892,336 bytes consed

;; ((("nega" . "posi") . 100) (("posi" . "posi") . 428) (("nega" . "nega") . 1596))
;; => Error rate: 0.04708098

この返り値は、例えば"nega"のものを"posi"と判定したケースが100件あったということを意味する。残りは正解しているので正答率は95.3%程度となる。論文では99%くらい出るとあるのでちょっと低い。なんで。Hivemailでも99%くらい出る模様。

メタパラメータの指定

メタパラメータとして指定できるものは、

  • 決定木の数 (デフォルトは500)
  • 元のデータセットから重複を許してサンプリングしたものをデータセットとして各決定木を学習するのだが、その際のサンプルサイズ (デフォルトは元のデータセット全体のサイズ)
  • 元の特徴量からサンプリングしたものを各決定木の特徴量として使うのだが、その際のサンプリングする特徴の数 (デフォルトはフルの特徴量を使うが、よく使われるのは元の特徴量のサイズの平方根)

これらを調整してより小さなランダムフォレストを作ると、

(defparameter *small-forest*
  (clml.decision-tree.random-forest:make-random-forest
   *mushrooms-train.clml* "class"
   :tree-number 100
   :data-bag-size (floor (/ (length *mushrooms-train*) 10))
   :feature-bag-size (floor (sqrt *mushrooms-dim*))))

;; Evaluation took:
;;   1.217 seconds of real time
;;   4.279771 seconds of total run time (4.191569 user, 0.088202 system)
;;   [ Run times consist of 0.141 seconds GC time, and 4.139 seconds non-GC time. ]
;;   351.68% CPU
;;   4,125,539,396 processor cycles
;;   2,282,142,096 bytes consed

;; => Error rate: 0.041902073

となってかなり高速になり、精度も若干上がった。

番外編: cl-online-learningでもやってみる

(defparameter arow-learner (clol:make-arow *mushrooms-dim* 0.1d0))
(loop repeat 10 do
  (clol:train arow-learner *mushrooms-train*)
  (clol:test arow-learner *mushrooms-test*))

;; Accuracy: 91.99623%, Correct: 1954, Total: 2124
;; Accuracy: 94.3032%, Correct: 2003, Total: 2124
;; Accuracy: 93.92655%, Correct: 1995, Total: 2124
;; Accuracy: 93.87947%, Correct: 1994, Total: 2124
;; Accuracy: 93.83239%, Correct: 1993, Total: 2124
;; Accuracy: 93.87947%, Correct: 1994, Total: 2124
;; Accuracy: 93.83239%, Correct: 1993, Total: 2124
;; Accuracy: 93.83239%, Correct: 1993, Total: 2124
;; Accuracy: 93.83239%, Correct: 1993, Total: 2124
;; Accuracy: 93.83239%, Correct: 1993, Total: 2124

;; Evaluation took:
;; 0.018 seconds of real time
;; 0.018714 seconds of total run time (0.014723 user, 0.003991 system)
;; 105.56% CPU
;; 64,026,632 processor cycles
;; 786,432 bytes consed

(defparameter scw-learner (clol:make-scw *mushrooms-dim* 0.999d0 0.001d0))
(loop repeat 10 do
  (clol:train scw-learner *mushrooms-train*)
  (clol:test scw-learner *mushrooms-test*))

;; Accuracy: 38.93597%, Correct: 827, Total: 2124
;; Accuracy: 66.00753%, Correct: 1402, Total: 2124
;; Accuracy: 69.96233%, Correct: 1486, Total: 2124
;; Accuracy: 81.40301%, Correct: 1729, Total: 2124
;; Accuracy: 88.46516%, Correct: 1879, Total: 2124
;; Accuracy: 93.36158%, Correct: 1983, Total: 2124
;; Accuracy: 96.65725%, Correct: 2053, Total: 2124
;; Accuracy: 97.834274%, Correct: 2078, Total: 2124
;; Accuracy: 98.44633%, Correct: 2091, Total: 2124
;; Accuracy: 98.06968%, Correct: 2083, Total: 2124

;; Evaluation took:
;;   0.038 seconds of real time
;;   0.037766 seconds of total run time (0.037766 user, 0.000000 system)
;;   100.00% CPU
;;   128,015,604 processor cycles
;;   2,064,384 bytes consed

このデータセットではSCW-Iが比較的良い性能を出した。過学習しがちなので早めに止める必要がある。

まとめ/感想

  • CLMLのランダムフォレストを試してみた
  • データセットが型指定しないunspecialized-datasetであり、遅い
  • あまり最適化されていないのでまだまだ速くなりそう
  • 精度も何故か良くない
  • CLMLの実装を叩き台にしてオンラインランダムフォレストを実装してみるつもり

2017-02-03

Common LispでかんたんWebスクレイピング

WebスクレイピングとはWebから情報を自動的に集めてくるクローラを実装するということである。これを実現するにはHTTPクライアントとHTMLパーサ、そしてパースされた木構造から必要な情報を探索、抽出するセレクタがあればいい。Common Lispにはそれぞれに複数のライブラリがあるが、今回はHTTPクライアントにDexadorHTML/XMLパーサにPlumpCSSセレクタにCLSSを使う。これらのライブラリは全てQuicklispから入る。

(ql:quickload :dexador)
(ql:quickload :plump)
(ql:quickload :clss)

例としてこのロイターの記事 堅調地合い、1万8000円へ戻りを試す展開に=来週の東京株式市場 を分析してみる。

HTTPクライアント: Dexador

まずHTTPクライアントでHTMLを取ってくる。これにはdexadorのget関数を使う。

(defparameter article-html (dex:get "http://jp.reuters.com/article/idJPL3N0U325520141219"))

dex:getは取得したHTML文字列、ステータス、メタ情報のハッシュ表、URI、ストリームを多値で返す。

"<!doctype html><html><head>
<title>
            堅調地合い、1万8000円へ戻りを試す展開に=来週の東京株式市場
|ロイター</title>|
... 中略 ...
</html>
"
200
#<HASH-TABLE :TEST EQUAL :COUNT 14 {1003F285C3}>
#<QURI.URI.URI-HTTP http://jp.reuters.com/article/idJPL3N0U325520141219>
#<SB-SYS:FD-STREAM for "socket 192.168.11.12:43208, peer: 52.222.193.218:80" {1003DD4B13}>

HTMLパーサ: Plump

次に、plumpのparse関数でHTML文字列をパースする。これは木構造のルートに相当するCLOSオブジェクトを返す。

(defparameter parse-tree (plump:parse article-html))

;; => #<PLUMP-DOM:ROOT {1006E77F53}>

このオブジェクトの子を表示してみると、

(plump:children parse-tree)

;; #(#<PLUMP-DOM:COMMENT {1005D8C563}> #<PLUMP-DOM:TEXT-NODE {1005D8C853}>
;;   #<PLUMP-DOM:COMMENT {1005D8CF53}> #<PLUMP-DOM:TEXT-NODE {1005D8D253}>
;;   #<PLUMP-DOM:COMMENT {1005D8DB73}> #<PLUMP-DOM:TEXT-NODE {1005D8DE93}>
;;   #<PLUMP-DOM:COMMENT {1005D8E4A3}> #<PLUMP-DOM:TEXT-NODE {1005D8E773}>
;;   #<PLUMP-DOM:COMMENT {1005D8ECF3}> #<PLUMP-DOM:TEXT-NODE {1005D8F053}>
;;   #<PLUMP-DOM:DOCTYPE html> #<PLUMP-DOM:ELEMENT html {1005D8FDC3}>
;;   #<PLUMP-DOM:TEXT-NODE {1006274133}>)

このうちtext-nodeオブジェクトが文字列を持っている。木構造を走査してtext-nodeの持つ文字列だけを連結する関数を定義してみるとこうなる。

(defun node-text (node)
  (flet ((cat (strs) (reduce (lambda (s1 s2) (concatenate 'string s1 s2)) strs)))
    (let ((text-list nil))
      (plump:traverse node
                      (lambda (node) (push (plump:text node) text-list))
                      :test #'plump:text-node-p)
      (cat (nreverse text-list)))))

普通に再帰で書いても行数はあまり変わらないと思うが、せっかくtraverse関数が用意されていたので使ってみた。

CSSセレクタ: CLSS

jQueryのように木構造からCSS要素を指定して部分木を抜いてくることができる。例えば、Plumpでパースした木からarticleTextというIDを持つ最初のノードを取り出すには以下のようにする。

(defparameter sub-tree (aref (clss:select "#articleText" parse-tree) 0))

この部分木に対して先ほど定義したnode-textを使うと記事の本文が得られる。

(node-text sub-tree)

;; "
;; [東京 19日 ロイター] - 来週の東京株式市場は堅調な地合いが続く見通しだ。 (以下略
;; "

同様に記事タイトルやジャンルなども取ってこられる。

(node-text (aref (clss:select ".article-headline" parse-tree) 0))
; => "堅調地合い、1万8000円へ戻りを試す展開に=来週の東京株式市場"

(node-text (aref (clss:select ".article-section" parse-tree) 0))
; => "Markets"

まとめとか

実際のページのソースを見てみると本文の部分はdivやspanが入り乱れているので単純な文字列のパターンマッチだとめんどくさそうに思えるが、HTMLをパースして木構造とすることで一気に扱いやすくなる。

ブラウザのインスペクタでクラス/IDを調べてclss:selectで指定するだけなので簡単。

ロイターの場合、サイトマップのXMLファイルがあるので上と同様に分析してURLのリストを取り出すことができる。

2016-12-19

Common Lispが機械学習に向いていると考えるこれだけの理由

Lisp Advent Calendar 2016参加記事

ここ数年ディープラーニングの出現をきっかけにAIが再び盛り上がっているので、いよいよLispの復権があるかと思いきや、ないので(泣)、多少なりともLispに興味を持ってもらえるように、LispとAIの関係について私見を述べておこうと思う。Lispといっても色々あるが、この記事では主にCommon Lispの話になる。

Lispというとどうしても過去の記号処理的AIと結びつけられてしまい、機械学習を駆使するような現代のAIでは役に立たないように思われがちなのだが、これは大体誤解である。少なくともCommon Lispは現代的なAI開発に適した特徴を備えている。まず、AI実装のためのプログラミング言語に必要とされる特徴は何なのかを明らかにするために、AIの歴史から考えてみたい。

AIの歴史

初期の記号処理的AI(以降は記号AIと呼ぶ)ではLispやPrologが実装言語として広く採用されてきた歴史がある。記号処理とはその名の通り記号を操作対象とする処理のことで、具体的には、エキスパートシステム、数式処理、プログラミング言語のコンパイラなど、論理的な推論や構造の変換を伴うものが多い。それらの記号ベースのデータ構造はリストで表現されることが多かったので、リストの取り扱いを得意とするLispが採用されてきたわけだ。*1 記号AIは一定の成功をおさめたが、現実世界の複雑な問題に適用しようとするとフレーム問題や記号接地問題の壁にぶち当たった。結局、古典的な記号AIでは事物の特徴をどのように取り出し、抽象化し、関連付ければいいのか、それをどこまでやったらいいのかといったことを解決できなかったのだ。

現実世界でも適用可能な妥当なルールベースを作りこむことが難しいことが分かってきたので、その後は大量のデータからボトムアップ的に学習して自動的に知識表現を獲得してやろうという流れになり、ニューラルネットなどの機械学習手法が発達した。最近のディープラーニングなどはその延長線上にある。

機械学習は数値計算

ほとんどの機械学習のアルゴリズムは何らかの目的関数を最適化することによって学習を実現している。例えばニューラルネットの学習であれば、モデルからの予測値と実測値の誤差を目的関数として、その勾配(微分)を取り、誤差を最小化させる方向にモデルパラメータを少し動かすことを繰り返す。このことからも分かるように、記号AIのプログラミングはリスト処理が中心だったのに対して、機械学習AIのプログラミングは数値計算が中心となる。従って、複雑な数値計算をいかに簡潔に記述でき、いかに効率的に計算できるかがキモになる。

より高水準に。より低水準に。

コンピュータの性能向上にともない、最終的な実行速度よりも開発効率を重視するというトレンドが生まれた。研究者は短時間でアルゴリズムを実装して実験する必要があり、自然とAI分野ではRやMATLAB、Pythonなどの動的で高水準な言語に人気が集まった。これらは基本的には逐次実行のスクリプト言語であり、実行速度は遅い。

これに対する戦略として、これらの言語では外部の数値計算ライブラリを呼び出す何らかの方法を持っている。具体的には、計算処理をある程度まとまった単位にまとめて、Cなどの低水準言語で書かれた数値計算ライブラリにまるごと送り出す。例えば、ループでやるような処理をベクトルに対するマップ操作として記述するとか、ベクトルの計算ならば、小単位のバッチ処理にまとめて行列の演算に置き換えてから外部ライブラリに渡すなどのテクニックを用いることになる。

しかしいつでもこれができるとは限らない。例えばリアルタイムのオンライン処理には向かないし、そもそも逐次処理を必要とする学習アルゴリズムでは使えない。そういう場合は結局Cなどで書いてコンパイルしたものを呼び出す必要がある。*2 こうなってくると、実行速度を出すためにアルゴリズムの選択やプログラミングスタイルに制約がかかることになり、本末転倒ということになりかねない。

実はCommon Lispは数値計算に向いている

もともとLispの特徴として、柔軟で強力な抽象化機構を持ち、時としてプログラマ自身が構文すら自由に拡張できる高水準言語だということがあった。それに加えてCommon LispにはSBCLなどのオープンソースの高品質なネイティブコンパイラがあり、Cで書かれたライブラリに簡単にアクセスできる仕組みCFFIがある。さらに最近ではインターネットからライブラリをダウンロード、インストール、ロードまでを一貫してこなすライブラリ管理システムQuicklispや、処理系のバージョンを管理したりデプロイを行なえるツールRoswellなども出てきて、いよいよ便利になりつつある。

一部ではLispはスクリプト言語で遅くて数値計算には向かないというイメージがあるようだが、SBCLなどはJITコンパイラであるし、現在ではコンパイル型言語の中でもむしろ速い部類に入る。この話題の概要としては、Gauche作者のshiroさんのエントリがとても分かりやすかった。

速いコードを書くための具体的な方法論として参考になる情報源をいくつか挙げておくと、

Common Lispが速いのは、動的言語にも関わらず、最適化宣言と型宣言をオプションとして付けることができるためだ。動的言語が遅いと言われるのは、主にオブジェクトの型情報を実行時に判定する必要があるためで、極端な話、全ての変数に型宣言を付けて、最適化宣言で実行時の型チェックを切れば、Common LispコンパイラはCとほぼ同等のネイティブコードを吐き出す。さらに、これらの宣言はあらゆるレベルで付けることができるので、関数単位、ブロック単位で、本当に必要な部分にのみ集中して最適化することができる。

そして、往々にしてそのようなボトルネックはプログラム全体のごく一部にすぎない。大抵のCommon Lisp処理系には、プロファイラという、プログラムのどこの部分に計算時間が割かれているかを調べるツールが付いており、ボトルネックを探すのに役に立つ。また、コンパイルした関数を個別にディスアセンブルすることもできる。アセンブリコードを見ることによって、実行時の型変換がどこで起こっているかや、メモリアロケーションがどこで発生しているかなどが分かるので、これもチューニングの手掛かりになる。*3

以上をまとめると、Common Lispの開発サイクルは以下のようになる。

  1. 動的で高水準な言語機能を使って迅速にプロトタイピングする
  2. プロファイリングでボトルネックを探す
  3. 最適化宣言、型宣言を付けてコンパイル
  4. ディスアセンブルしてネイティブコードを見ながら最適化

これら全てが開発環境の中から出ることなく、対話的に行うことができる。これにより開発サイクルが短く、試行錯誤できる回数が多くなり、結果として最適なアルゴリズムを見つけ出すチャンスが増える。往々にして「Common LispでCよりも速いコードが書ける」というのは、最高速度のことというよりは、この開発サイクル自体の効率性が高いことを示している。

必要なら外部の数値計算ライブラリにもアクセスできる

Common Lispはそれ自体で速いとはいえ、巨大な行列同士の掛け算のように、マルチコアCPUやGPUの性能を極限まで引き出したいときには外部ライブラリに頼ることになる。CFFI経由で比較的簡単にCの共有ライブラリから関数を呼ぶことができるが、主な数値計算ライブラリにはラッパーライブラリが存在する。Intel MKLやOpenBLASのラッパーライブラリとしてはLLAがあり、CUDAによる行列計算のラッパーライブラリとしてはMGL-MATがある。

数値計算ライブラリに投げた後の処理はPythonのNumpyなどと同じなのでここでの速度差はあまりないが、FFIの部分でCommon Lispの方が若干速くなる。

素のCommon Lispの速さがあれば、機械学習で最も計算能力を必要とする学習時には外部の数値計算ライブラリを使い、予測時にはピュアCommon Lispのみを使うという選択肢も取れるようになる。そうすることでアプリケーションとして配布するときの外部の依存ライブラリを減らし移植性を増すことができる。

Common Lispの機械学習ライブラリ

Common Lispの機械学習ライブラリは数はあまり無いものの、重要なところは抑えているような気がする。とはいえまだまだ少ないので新規参入者求む!

まとめ

  • 機械学習AIに求められているのは迅速に開発できて、なおかつ実行速度も速いこと
  • Common Lispは基本的には高水準言語だが、ボトルネックになっているところを集中的に最適化することもできるので、低水準言語という側面も持つ
    • アルゴリズム選択の自由度が高く、ほとんどの処理をCommon Lisp内で完結できるので、実行速度と開発効率のバランスを取りやすい
    • 本当に重い処理は外部ライブラリに投げることもできる
  • 機械学習ライブラリも一応揃ってる
  • 新規参入しよう!

*1:Lispの由来はリスト処理(LISt Processing)から来ているという話がある

*2:一応、PythonであればCythonなどのネイティブコードにコンパイルする仕組みもある。

*3:例えば、新たにオブジェクトを生成して返すようなコードは、結果受取り用の変数を引数として与えてそれに代入させればメモリアロケーションは発生せず、ガベージコレクションの時間を削減できる。

2016-12-03

Common Lispで書かれた形態素解析器cl-igoを使ってみた

cl-igoはCommon Lispから使える形態素解析器で、辞書にはmecab互換の辞書が使える。

roswellから入るようにgithubにミラーを作ったので、

ros install masatoi/charseq masatoi/cl-igo

でインストールできる。SBCL推奨とのこと。

igoのバイナリ辞書を作る

mecab-ipadic-2.7.0-20070801.tgzとigo-0.4.5.jarが同じディレクトリに入っているとして、

tar xzvf mecab-ipadic-2.7.0-20070801.tar.gz
java -cp ./igo-0.4.5.jar net.reduls.igo.bin.BuildDic ipadic mecab-ipadic-2.7.0-20070801 EUC-JP

でipadicというディレクトリができている。~/igo/ipadicにでも置いておくとする。

使ってみる

(ql:quickload :igo)

;; 辞書を読み込む
(igo:load-tagger "/home/wiz/igo/ipadic/")

(igo:parse "庭には二羽にわとりがいる。")
(("庭" "名詞,一般,*,*,*,*,庭,ニワ,ニワ" 0) ("に" "助詞,格助詞,一般,*,*,*,に,ニ,ニ" 1)
 ("は" "助詞,係助詞,*,*,*,*,は,ハ,ワ" 2) ("二" "名詞,数,*,*,*,*,二,ニ,ニ" 3)
 ("羽" "名詞,接尾,助数詞,*,*,*,羽,ワ,ワ" 4) ("にわとり" "名詞,一般,*,*,*,*,にわとり,ニワトリ,ニワトリ" 5)
 ("が" "助詞,格助詞,一般,*,*,*,が,ガ,ガ" 9) ("いる" "動詞,自立,*,*,一段,基本形,いる,イル,イル" 10)
 ("。" "記号,句点,*,*,*,*,。,。,。" 12))
(igo:wakati "庭には二羽にわとりがいる。")
("庭" "に" "は" "二" "羽" "にわとり" "が" "いる" "。")

mecab-ipadic-neologdのインストール

mecab-ipadic-neologdは新語などを大幅に増やした辞書で、SNSのデータを分析したいようなときに重要になる。

Ubuntu14.04でのインストール例は、

# 必要パッケージをインストール
sudo apt-get install mecab libmecab-dev mecab-ipadic-utf8 git make curl xz-utils file

git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git
cd mecab-ipadic-neologd/

# 途中で確認が出るのでyesを入力
./bin/install-mecab-ipadic-neologd -n

# インストール先
echo `mecab-config --dicdir`"/mecab-ipadic-neologd"
# /usr/lib/mecab/dic/mecab-ipadic-neologd

# 動作確認
mecab -d /usr/lib/mecab/dic/mecab-ipadic-neologd

igoのバイナリ辞書を作る

mecab-ipadic-neologdはUTF-8のみ対応ということなので、文字コードにUTF-8を指定してバイナリ辞書を作る。

cd build/
java -cp ../../igo-0.4.5.jar net.reduls.igo.bin.BuildDic ipadic-neologd mecab-ipadic-2.7.0-20070801-neologd-20161201/ UTF-8

これでipadic-neologdというディレクトリができる。これはJava版のIgoからなら以下のようにして使える。

java -cp ../../igo-0.4.5.jar net.reduls.igo.bin.Igo ipadic-neologd

しかしCommon Lisp版はEUC-JPのみ対応のようで、igo:load-taggerで読み込もうとするとエラーになってしまう。うーむ。

2016-12-02

NumPy vs Common Lisp 実行時間のみの比較

NumPyとCommon Lispの速度比較記事。処理系の起動時間も測ってしまっているので処理自体にかかっている時間を測って比較してみる。

環境は

  • Core i5 4670 3.40 GHz
  • Ubuntu 14.04 LTS

Python 3.4.3 + Numpy

import numpy as np
import time

N = 100000

# Python版
def sumup(n):
    return sum(range(1, n + 1))

# NumPy版
def sumup(n):
    return np.arange(1, n + 1).sum()

def main():
    print("python with numpy start.")
    result = {}
    for count in range(1, N + 1):
        result[count - 1] = sumup(count)
    print("python with numpy end.")

start = time.time()
main()
elapsed_time = time.time() - start
print("elapsed_time:{0}".format(elapsed_time))

SBCL 1.3.11

(defparameter *n* 100000)

;; 1. 引数のみ型宣言
(defun sumup1 (n)
  (declare (type fixnum n))
  (let ((sum 0))
    (loop for i from 1 to n
          do (incf sum i))
    sum))

;; 2. 引数と局所変数で型宣言
(defun sumup2 (n)
  (declare (type fixnum n))
  (let ((sum 0))
    (declare (type fixnum sum))
    (loop for i from 1 to n
          do (incf sum i))
    sum))

;; 3. 引数と局所変数で型宣言 + 最適化宣言、実行時型チェック無効
(defun sumup3 (n)
  (declare (type fixnum n)
           (optimize (speed 3) (safety 0)))
  (let ((sum 0))
    (declare (type fixnum sum))
    (loop for i from 1 to n
          do (incf sum i))
    sum))

;; 4. 引数と局所変数で型宣言 + 最適化宣言、実行時型チェック無効、loop内で使う変数iも型宣言
(defun sumup4 (n)
  (declare (type fixnum n)
           (optimize (speed 3) (safety 0)))
  (let ((sum 0))
    (declare (type fixnum sum))
    (loop for i fixnum from 1 to n
          do (incf sum i))
    sum))

;; 5. 引数と局所変数で型宣言 + 最適化宣言、実行時型チェック無効、loop内で使う変数iも型宣言、引数と返値の型を宣言する
(declaim (ftype (function (fixnum) fixnum) sumup5))
(defun sumup5 (n)
  (declare (type fixnum n)
           (optimize (speed 3) (safety 0)))
  (let ((sum 0))
    (declare (type fixnum sum))
    (loop for i fixnum from 1 to n
          do (incf sum i))
    sum))

(defun main ()
  (print "common lisp start.")
  (loop for count from 1 to *n*
        collect (sumup5 count))
  (print "common lisp end."))

(time (main))

結果

結果はこのようになる。

ftypeの宣言があまり効いていないが、なんにせよCommon Lispに適切なチューニングを施すことによってNumPyを使ったときよりも3倍以上も速くなっていることが分かる。