記録は作業の証

鉄道とコンピュータ

ゼロから作るDeep LearningをCommon Lispで書き直す(ステップ19)

ステップ19 変数を使いやすく

このステップでは次の機能を実装する。

  • 変数に名前を付けられるようにする。
  • 変数から配列の形状を知ることができるアクセサを実装する。
  • プリティプリンタを実装する。

変数に名前を付ける

<variable>クラスにnameスロットを足し、コンストラクタ関数から指定できるようにする。

(defclass <variable> ()
  ((data :initarg :data :accessor @data)
   (name :initarg :name :initform nil :accessor @name) ; 名前を記録する
   (gradient :initform nil :accessor @gradient)
   (creator :initform nil :accessor @creator)
   (generation :initform 0 :accessor @generation)))

(defun <variable> (data &optional name) ; (<variable> #(3 4) "var A")
  (make-instance '<variable> :data data :name name))

アクセサ関数を増やす

NumPyのそれを参考にarray-operationsの関数を使って実装する。 ndarrayに対するlenって最初の次元の数であってるんだろうか?

(defmethod shape ((var <variable>))
  "list of array dimensions"
  (aops:dims (@data var)))

(defmethod ndim ((var <variable>))
  "number of array dimenstions"
  (aops:rank (@data var)))

(defmethod size ((var <variable>))
  "number of elements in the array"
  (aops:size (@data var)))

(defmethod dtype ((var <variable>))
  "data type of array's elements"
  (aops:element-type (@data var)))

(defmethod len ((var <variable>))
  "number of array's first dimention"
  (aops:dim (@data var) 0))

プリティプリンタを実装する

デバッグのときに変数の中身が容易に見えると便利なのでprint-object総称関数にメソッドを実装する。 CLHSの書式文字列の説明と下記の記事を参考にした。

qiita.com

(defmethod print-object ((var <variable>) stream)
  (print-unreadable-object (var stream :type t :identity nil)
    (format stream
            "~:@_~<data: ~W ~_name: ~W ~_gradient: ~W ~_creator: ~W ~_generation: ~W~:>"
            (list (@data var) (@name var)
                  (@gradient var) (@creator var) (@generation var)))))

(<variable> #(3 2) "var A")
 ; => #<<VARIABLE> data: #(3 2) name: "var A" gradient: NIL creator: NIL generation: 0>

(defmethod print-object ((func <function>) stream)
  (print-unreadable-object (func stream :type t :identity nil)
    (format stream
            "~<generation: ~W~:>"
            (list (@generation func)))))

そのほか

array-operationsのマニュアルを読んでいたら、vectorize-reduceというマクロがあることに気がついたので、勾配確認のときに書いたall-close関数を書き直して試してみた。最初のバージョンのall-close関数であるall-close-efvより、vectorize-reduceを使ったall-close-vrのほうが3割から4割短い時間で計算できることが分かった。マクロ展開してみると、all-close-efvではアキュムレータとなる配列を割り当てていたが、all-close-vrではそれがなかったため、おそらくそれが要因だろうと思う。

(defun all-close-efv (x y)
  (every (lambda (x) (<= x 1d-08))
         (aops:flatten
          (aops:vectorize (x y)
            (/ (abs (- x y)) (abs x))))))

(defun all-close-vr (x y)
  (>= 1d-08 (aops:vectorize-reduce #'max (x y)
              (/ (abs (- x y)) (abs x)))))

(let ((x (aops:generate* 'double-float (lambda () (1+ (random 1.0d0))) '(5000 5000)))
      (y (aops:rand '(5000 5000) 'double-float)))
  ;; warm-up
  (all-close-efv x y)
  (all-close-vr x y)
  ;; benchmark
  (time (all-close-efv x y))
  (time (all-close-vr x y)))

;; Evaluation took:
;;   1.400 seconds of real time
;;   1.381522 seconds of total run time (1.283582 user, 0.097940 system)
;;   [ Real times consist of 0.500 seconds GC time, and 0.900 seconds non-GC time. ]
;;   [ Run times consist of 0.499 seconds GC time, and 0.883 seconds non-GC time. ]
;;   98.71% CPU
;;   5,287,418,680 processor cycles
;;   2,599,993,824 bytes consed

;; Evaluation took:
;;   0.939 seconds of real time
;;   0.944718 seconds of total run time (0.894239 user, 0.050479 system)
;;   [ Real times consist of 0.090 seconds GC time, and 0.849 seconds non-GC time. ]
;;   [ Run times consist of 0.087 seconds GC time, and 0.858 seconds non-GC time. ]
;;   100.64% CPU
;;   3,585,169,280 processor cycles
;;   2,400,026,576 bytes consed

ほかにはzeros-like関数やones-like関数が特殊化された配列(:element-typetでない)に対応できていなかったので、array-operationsを使って書き直した。 <function>クラスに対するforwardメソッドやbackwardメソッドは消しておく。単にエラーメッセージを出力するデフォルトメソッドを用意するより、メソッドが存在しないときにデバッガに落ちる方がCommon Lispらしいやりかただろう。

gist.github.com.