ゼロから作るDeep Learning. Common Lispで学ぶディープラーニングの理論と実装(1)

ゼロから作るDeep LearningのコードをCommon Lispに移植しながら学習していくシリーズです。

Common Lisp入門

1.1 Common Lispとは

1.2 処理系のインストール

ここではインストールに際しての注意点を説明します。

1.2.1 処理系

Common LispはANSI仕様が存在するため処理系は複数あります。 ここでは最も開発が活発で実行速度の速さでも群を抜くSBCLを使用します。

1.2.2 Dependencies

必要な便利ライブラリはドシドシ使っていきます。

1.2.3 Roswell

Common Lispのインストール方は様々ありますが、ここでは処理系マネージャであるRoswellを推奨します。 Roswellのインストールに関してはここを参照してください。

1.3 REPL

処理系のREPL(対話的インターフェース)にはエディタからアクセスできると便利です。 Emacsユーザーはslime、VIMユーザーはvlimeを使うと幸せになれます。 もしくはrlwrapを使ったほうが幸せになれるかたもいるかもしれません。

1.3.1 Arithmetic

加算や乗算などの算術計算は以下のように行います。

* (- 1 2 3)
-4

* (* 4 5 6)
120

* (/ 7 5 3)
7/15

* (expt 3 2)
9

除算の結果が分数になっている点要注意。 少数が欲しい場合は引数か返り値を明示的に少数にする必要があります。

* (/ 7 5.0)
1.4

* (float (/ 7 5))
1.4

1.3.2 Data types

プログラミングではデータ型(data type)というものがあります。 データ型とはデータの性質を表すもので、たとえば、整数、少数、文字列といった型があります。 Common LispにはTYPE-OFという関数があり、この関数でデータの型を調べることができます。

* (type-of 10)
(INTEGER 0 4611686018427387903)

* (type-of 1.5)
SINGLE-FLOAT

* (type-of "hoge")
(SIMPLE-ARRAY CHARACTER (4))

なおTYPE-OFの返り値は型指定子(type specifier)ですが、帰ってくる型指定子は処理系依存です。 同じ処理系であってもバージョン違いで返り値が異なる場合があります。 TYPE-OFは通常コードには書きません。 (あくまでREPLと対話的に調べたい時のためのもの。) コード内で型をチェックしたい場合はTYPEPを使います。

* (typep 10 'integer)
T

* (typep 10 'float)
NIL

また、型により条件分岐を行いたい場合はTYPECASEのファミリーが便利に使えます。

* (typecase 10
    (float :no)
    (integer :yes))
:YES

NOTE

歴史的由来により厄介な点が一つあります。

ここでいう歴史的由来とは素のLispの上にオブジェクトシステムが後づけで開発されたことです。 それ自体はLispという言語の柔軟さや強力さを表していることなのですが、後追いで参入してきた初心者にはわかりづらいものとなっている問題点があります。

全てのクラスは型ですが、全ての型がクラスを持つとは限らないのです。

データのクラスを調べたい場合はCLASS-OF関数を使います。

* (class-of 10)
#<BUILT-IN-CLASS FIXNUM>

* (class-of 1.5)
#<BUILT-IN-CLASS SINGLE-FLOAT>

* (class-of "hoge")
#<BUILT-IN-CLASS SB-KERNEL:SIMPLE-CHARACTER-STRING>

具体的な事例としてはTNILBOOLEAN型でもありますが、BOOLEAN型に対応するクラスは存在しません。

* (typep t 'boolean)
T

* (find-class 'boolean)
; Error

1.3.3 Variable

グローバルな変数はDEFPARAMETERないしDEFVARで宣言してから使います。

* (defvar *x* 10) ; 宣言と初期化。グローバル変数名の左右に`*`をつけるのは作法。
*X*               ; DEFPARAMETER式は宣言された変数名を返す。

* (print *x*) ; *X*の値を出力。
10            ; 出力(副作用)
10            ; PRINT式は出力した値を返す。

* (setf *x* 100) ; 代入には`SETF`を使う。
100              ; SETF式は最後に代入した値を返す。

* (defvar *y* 3.14)
*Y*

* (* *x* *y*)
314.0

* (type-of (* *x* *y*))
SINGLE-FLOAT

Common Lispは動的型付けの言語なので変数に型宣言は必要ありません。 ですが型宣言ができないわけではありません。 型宣言をした場合の振る舞いは処理系依存です。 ここで採用しているSBCLはオープンソース処理系の中でも特に型に厳しい処理系です。 型宣言をした場合、コンパイル時に静的な型チェックが行われます。

なお、「;」はコメントで、それ以降の文字は無視されます。

1.3.4 List

LIST関数でリストオブジェクトを作れます。

* (defvar *l* (list 1 2 3 4 5))
*L*

* *l* ; 中身の確認。
(1 2 3 4 5)

* (length *l*) ; 長さの取得。
5

* (nth 0 *l*) ; 最初の要素にアクセス。
1

* (nth 4 *l*)
5

* (setf (nth 4 *l*) 99) ; 代入。
99

* *l*
(1 2 3 4 99)

リスト要素へのアクセスにはNTH関数を使います。 Common Lispは汎変数をサポートしており、値を参照する式は通常、代入できます。 上記例ではNTH式を経由して99を代入しています。

部分リストの取得にはSUBSEQ関数を使います。

* (subseq *l* 0 2) ; インデックスの0番目から2番目まで(2番目は含まない)取得。
(1 2)

* (subseq *l* 1) ; インデックスの1番目から最後まで取得。
(2 3 4 99)

1.3.5 Hash-table

キーと値のペアを格納する専用のオブジェクトとしてHASH-TABLEがサポートされています。

* (defvar *ht* (make-hash-table)) ; 空のハッシュテーブルを作成。
*HT*

* (setf (gethash :key *ht*) :value) ; 新しいペアを代入。
:VALUE

* (gethash :key *ht*) ; 参照。
:VALUE

ハッシュテーブルは多くの処理系で中身が見えません。

* *ht*
#<HASH-TABLE :TEST EQL :COUNT 1 {1007F58EF3}>

* (print *ht*)                                ; PRINTしても
#<HASH-TABLE :TEST EQL :COUNT 1 {1007F58EF3}> ; 中身は見えない。
#<HASH-TABLE :TEST EQL :COUNT 1 {1007F58EF3}> ; PRINTは表示したオブジェクトを返す。

中身を見たい場合はINSPECT関数を使います。 INSPECTの表示方法は処理系依存ですのでここでは割愛します。

* (inspect *ht*) ; 表示例は割愛。

1.3.6 Boolean

Common Lispは汎ブールをサポートする言語です。 NILが唯一の偽値(FALSE)で、それ以外はすべてTRUEと解釈されます。

AND

ANDマクロは左から順に評価していき、NILを返すフォームに出会うとそれを返り値とします。 どのフォームもNILを返さなかった場合、最後のフォームの返り値がANDマクロの返り値となります。

* (and nil t nil)
NIL

* (and 1 2 3)
3

OR

ORマクロは左から順に評価していき、非NIL値を返すフォームに出会うとそれを返り値とします。 どのフォームも非NIL値を返さなかった場合、NILORマクロの返り値となります。

* (or nil 1 2 3)
1

* (or nil nil nil)
NIL

NOT

NOT関数はブールを反転させます。

* (not nil)
T

* (not 0)
NIL

BOOLEAN

より厳格な型としてブーリアン型も定義されています。 この場合TNILのみがブーリアン型と解釈されます。

* (typep nil 'boolean)
T

* (typep 0 'boolean)
NIL

ブーリアン型はクラスではありません。

* (find-class 'boolean)
; Error.

1.3.7 If

条件に応じて処理を分岐する場合は分岐に応じて各種コマンドを使い分けます。

IF

分岐が二分木となる場合IFを使います。

* (let ((num (random 2)))
    (if (zerop num)
      :zero
      :one))

COND, CASE, TYPECASE

分岐が多分木となる場合CONDCASETYPECASEなどを使います。

* (let ((num (random 3)))
    (cond
      ((zerop num) :zero)
      ((evenp num) :even)
      ((oddp num) :odd)))

WHEN, UNLESS

エラーを投げたりするような副作用が目当ての場合はWHENUNLESSを使います。

* (defun my-evenp (num)
    (unless (integerp num)
      (error "~S is not integer." num))
    (zerop (/ num 2)))

1.3.8 Loop

Loop処理を行うにはLOOPマクロを使用します。

* (loop :for i :in '(1 2 3)
        :do (print i))
1
2
3
NIL

LOOPマクロは強力でできることが非常に多うございます。 初心者の方はとりあえずここなどで概要を学んでおくことをおすすめします。

1.3.9 Function

まとまりのある処理を関数として定義することができます。

* (defun hello ()
    (print :hello))
HELLO

また、関数は引数を取ることができます。

* (defun hello (name)
    (format t "Hello, ~A!" name))
HELLO

* (hello "guevara")
Hello, guevara!
NIL

1.4 Ros Script

1.4.1 Save file

1.4.2 Class

1.5 NumCL

NumclはNumpyのCommon Lisp cloneです。

1.5.1 Using numcl

Numclはライブラリです。 標準のCommon Lispには含まれません。 ライブラリをインストールするにはquicklispのコマンドを叩きます。 quicklispはunixのapt-getに相当するライブラリです。 roswellをインストールしているなら、roswellがquicklispをインストールしてくれているのでここではquicklispのインストール方法については触れません。

* (ql:quickload :numcl)
(:NUMCL)

なお、numclはコンパイル時にメモリを大量に消費します。 ヒープサイズが小さい場合、ヒープを食いつぶして途中で処理系が死にます。 その場合でも途中まではコンパイルが済んでいるので、改めて処理系を立ち上げ直し、繰り返しQUICKLOADをするといずれコンパイルは終了します。

追い込んでいないので数字はおおよそのものですが、ヒープが2ギガあると食いつぶすことはないようです。 ros経由で処理系を立ち上げている場合以下のようにしてヒープサイズを指定できます。

$> ros -v dynamic-space-size=2gb run

1.5.2 Making numcl array.

Numcl配列を作るにはNUMCL:ASARRAYを使います。

* (numcl:asarray '(1.0 2.0 3.0))
#(1.0 2.0 3.0)

1.5.3 Numcl arithmetics.

Numcl配列の算術計算例を示します。

* (defvar *x* (numcl:asarray '(1.0 2.0 3.0)))
*X*

* (defvar *y* (numcl:asarray '(2.0 4.0 6.0)))
*Y*

* (numcl:+ *x* *y*) ; 要素ごとの足し算。
#(3.0 6.0 9.0)

* (numcl:- *x* *y*)
#(-1.0 -2.0 -3.0)

* (numcl:* *x* *y*) ; Element-wise product
#(2.0 8.0 18.0)

* (numcl:/ *x* *y*)
#(0.5 0.5 0.5)

Broadcast

* (numcl:/ *x* 2.0)
#(0.5 1.0 1.5)

1.5.4 Numcl multi dimensional array.

* (defvar *a* (numcl:asarray '((1 2) (3 4)))) ; 多次元配列の作成。
*A*

* *a*            ; 中身の確認。
#2A((1 2) (3 4))

* (numcl:shape *a*)
(2 2)

* (numcl:dtype *a*)
(UNSIGNED-BYTE 4)
* (defvar *b* (numcl:asarray '((3 0) (0 6))))
*B*

* (numcl:+ *a* *b*) ; Matrix同士の足し算。
#2A((4 2) (3 10))

* (numcl:* *a* *b*) ; Matrix同士の掛け算。
#2A((3 0) (0 24))

* (numcl:* *a* 10) ; Broadcast.
#2A((10 20) (30 40))

1.5.5 Broadcast.

numpy用のものですがブロードキャストについては例えばここなどが参考になるかと思われます。

* (numcl:* (numcl:asarray '((1 2) (3 4))) (numcl:asarray '(10 20)))
#2A((10 40) (30 80))

1.5.6 Access element.

要素へのアクセスにはAREF関数を使います。

* (defvar *x* (numcl:asarray '((51 55) (14 19) (0 4))))
*x*

* (numcl:aref *x* 0)
#(51 55)

* (numcl:aref *x* 0 1)
55

shapeの返り値を利用することでお好みの繰り返しコマンドを使用できます。

* (dotimes (x (car (numcl:shape *x*)))
    (print (numcl:aref *x* x)))
#(51 55)
#(14 19)
#(0 4)
NIL
* *x*
#2A((51 55) (14 19) (0 4))

* (numcl:flatten *x*)
#(51 55 14 19 0 4)     ; <--- Main return value.
#(51 55 14 19 0 4 0 0) ; <--- Background vector.

* *x*
#2A((51 55) (14 19) (0 4)) ; <--- Numcl:flatten has no side effect.
* (numcl:take (numcl:flatten *x*) '((0 2 4)))
(51 14 0)

* (numcl:> *x* 15)
#2A((1 1) (0 1) (0 0))

* (numcl:take *x* (numcl:where *x* (lambda (x) (> x 15))))
(51 55 19)

1.6 Plot

ここではEazy-gnuplotを使います。

1.6.1 Drawing simple graph.

* (ql:quickload :eazy-gnuplot)
(:EAZY-GNUPLOT)

* (let* ((x (numcl:arange 0 6 0.1))
         (y (numcl:sin x)))
    (eazy-gnuplot:with-plots (*standard-output* :debug nil)
      (eazy-gnuplot:gp-setup :terminal '(pngcairo) :output "plot.png")
      (eazy-gnuplot:plot (lambda ()
                           (map nil (lambda (x) (format t "~%~A" x)) y))
                         :with '(:lines)))
    (uiop:run-program "display plot.png"))

Simple graph

1.6.2 Features.

* (let* ((x (numcl:arange 0 6 0.1))
         (y1 (numcl:sin x))
         (y2 (numcl:cos x)))
    (eazy-gnuplot:with-plots (*standard-output* :debug nil)
      (eazy-gnuplot:gp-setup :terminal :png :output "plot.png" :title "sin \\\\& cos"
                             :xlabel "x" :ylabel "y")
      (eazy-gnuplot:gp :set :encoding :utf8)
      (eazy-gnuplot:plot (lambda ()
                           (map nil (lambda (x) (format t "~%~A" x)) y1))
                         :with '(:lines :title "sin"))
      (eazy-gnuplot:plot (lambda ()
                           (map nil (lambda (x) (format t "~%~A" x)) y2))
                         :with '(:lines :title "cos" :linestyle "--")))
    (uiop:run-program "display plot.png"))

Sin and Cos

1.6.3 Showing images.

1.7 Summary.

本章で学んだこと。