ステップ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の書式文字列の説明と下記の記事を参考にした。
(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-type
がt
でない)に対応できていなかったので、array-operations
を使って書き直した。
<function>
クラスに対するforward
メソッドやbackward
メソッドは消しておく。単にエラーメッセージを出力するデフォルトメソッドを用意するより、メソッドが存在しないときにデバッガに落ちる方がCommon Lispらしいやりかただろう。