-
Notifications
You must be signed in to change notification settings - Fork 1
/
fsvd.lisp
499 lines (437 loc) · 19.6 KB
/
fsvd.lisp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
(in-package :fsvd)
;;; Possibly sparse matrix interface
(defgeneric height-of (matrix &key densep)
(:documentation "Return the number of rows of MATRIX. If DENSEP
return the number of non-empty rows."))
(defgeneric width-of (matrix &key densep)
(:documentation "Return the number of columns of MATRIX. If DENSEP
return the number of non-empty columns."))
(defgeneric size-of (matrix)
(:documentation "Return the number known cells in MATRIX. This is an
upper limit for the dense indices produced by MAP-MATRIX."))
(defgeneric dense-row-index (matrix row)
(:documentation "Number those rows that are not empty from 0. Return
NIL for empty rows.")
(:method ((matrix t) row)
row))
(defgeneric dense-column-index (matrix column)
(:documentation "Number those columns that are not empty from 0.
Return NIL for empty columns.")
(:method ((matrix t) column)
column))
(defgeneric map-matrix (function matrix)
(:documentation "Call FUNCTION for each non-empty cell of MATRIX.
FUNCTION is of four parameters: ROW, COLUMN, VALUE and DENSE-INDEX
where DENSE-INDEX is akin to a row major index except it doesn't skip
over empty cells. DENSE-INDEX is always less than the SIZE-OF the
MATRIX."))
(defmacro do-matrix (((row column value dense-index) matrix)
&body body)
"A simple, inefficient implementation of the macro interface to
iterate over MATRIX."
`(map-matrix (lambda (,row ,column ,value ,dense-index)
,@body)
,matrix))
(defgeneric do-matrix-macro-name (matrix)
(:method ((matrix t))
'do-matrix)
(:documentation "Return the name of the macro that provides a
hopefully efficient way to iterate over MATRIX. See DO-MATRIX for an
example."))
;;; Vector utilities
(deftype single-float-vector () '(simple-array single-float (*)))
(deftype 2d-single-float-array () '(simple-array single-float (* *)))
(defun make-v (length initial-element)
(make-array length :element-type 'single-float
:initial-element initial-element))
;;; Reading and writing float arrays
#+sbcl
(progn
(defun sync->fd (fd-stream)
(force-output fd-stream)
(let ((fd (sb-impl::fd-stream-fd fd-stream)))
(sb-unix:unix-lseek fd (file-position fd-stream) sb-unix:l_set)))
(defun sync<-fd (fd-stream)
(let ((fd (sb-impl::fd-stream-fd fd-stream)))
(file-position fd-stream
(sb-unix:unix-lseek fd 0 sb-unix:l_incr))))
(defun write-float-array (array fd-stream)
(declare (type (simple-array single-float (*)) array))
(sync->fd fd-stream)
(let ((fd (sb-impl::fd-stream-fd fd-stream)))
(sb-unix:unix-write fd
(sb-sys:vector-sap array)
0
(* 4 (length array))))
(sync<-fd fd-stream))
(defun read-float-array (array fd-stream)
(declare (type (simple-array single-float (*)) array))
(sync->fd fd-stream)
(let* ((l (* 4 (length array)))
(l2 (sb-unix:unix-read (sb-impl::fd-stream-fd fd-stream)
(sb-sys:vector-sap array)
l)))
(sync<-fd fd-stream)
(unless (= l l2)
(error "Read only ~S bytes out of ~S~%" l2 l))))
(defun write-float (float stream)
(declare (type single-float float))
(let ((v (make-array 1 :element-type 'single-float
:initial-contents (list float))))
(write-byte (sb-sys:sap-ref-32 (sb-sys:vector-sap v) 0) stream)))
)
#+allegro
(progn
(defun write-float-array (array stream)
(declare (type (simple-array single-float (*)) array)
(type excl:simple-stream stream))
(excl:write-vector array stream))
(defun read-float-array (array stream)
(declare (type (simple-array single-float (*)) array)
(type excl:simple-stream stream))
(let* ((l (* 4 (length array)))
(l2 (excl:read-vector array stream)))
(unless (= l l2)
(error "Read only ~S bytes out of ~S~%" l2 l))))
)
;;; Singular value decomposition
(defstruct sv
"SV is a pair of vectors. They are not of unit lenght. The singular
value is implicitly defined as the product of their euclidean norms."
(left nil :type (simple-array single-float *))
(right nil :type (simple-array single-float *)))
(defun create-sv (height width &optional (value 0.1))
(make-sv :left (make-v height value) :right (make-v width value)))
(deftype svd ()
"SVD consists of n SVs - pairs of left and right vector - of the
same sizes. The matrix SVD represents in this form is obtained by
summing pairwise the outer products of these vectors."
`(simple-array sv (*)))
(defun make-svd ()
"Create an empty SVD."
(make-array 0 :element-type 'sv :initial-element (create-sv 0 0)))
(defun append-to-svd (svd sv)
(let ((r (make-array (1+ (length svd)) :element-type 'sv)))
(replace r svd)
(setf (aref r (length svd)) sv)
r))
(defun compact-sv (sv matrix)
"Take SV that uses sparse indices of MATRIX and turn it into one
that uses dense indices."
(flet ((compact (target source map)
(loop for i below (length source) do
(let ((dense-i (funcall map matrix i)))
(when dense-i
(setf (aref target dense-i) (aref source i)))))))
(let ((new-sv (create-sv (height-of matrix :densep t)
(width-of matrix :densep t))))
(compact (sv-left new-sv) (sv-left sv) #'dense-row-index)
(compact (sv-right new-sv) (sv-right sv) #'dense-column-index)
new-sv)))
(defun expand-sv (sv matrix)
"Take SV that uses dense indices of MATRIX and turn it into one that
uses normal indices."
(flet ((expand (target source map)
(loop for i below (length target) do
(let ((dense-i (funcall map matrix i)))
(when dense-i
(setf (aref target i) (aref source dense-i)))))))
(let ((new-sv (create-sv (height-of matrix :densep nil)
(width-of matrix :densep nil))))
(expand (sv-left new-sv) (sv-left sv) #'dense-row-index)
(expand (sv-right new-sv) (sv-right sv) #'dense-column-index)
new-sv)))
(defun save-svd (svd filename)
"Write the content of SVD to FILENAME in a reasonably compact form."
(ensure-directories-exist filename)
(with-open-file (stream filename :direction :output
:if-does-not-exist :create
:if-exists :supersede)
(let ((sv (if (zerop (length svd))
(create-sv 0 0)
(aref svd 0))))
(format stream "~S~%" (list :length (length svd)
:left-size (length (sv-left sv))
:right-size (length (sv-right sv)))))
(loop for sv across svd do
(write-float-array (sv-left sv) stream)
(write-float-array (sv-right sv) stream)))
(values))
(defun load-svd (filename)
"Return the SVD loaded from FILENAME."
(with-open-file (stream filename)
(destructuring-bind (&key length left-size right-size)
(read-from-string (read-line stream))
(let ((svd (make-array length :element-type 'sv)))
(loop for i below length do
(let ((sv (create-sv left-size right-size)))
(read-float-array (sv-left sv) stream)
(read-float-array (sv-right sv) stream)
(setf (aref svd i) sv)))
svd))))
(defun svd-value (svd row column &key (base-value 0.0) (clip #'identity))
"Return the value of the matrix represented by SVD at ROW and
COLUMN. Start the summation from BASE-VALUE and CLIP the current sum
to some valid range if any after every pass."
(let* ((clip (coerce clip 'function))
(sum (funcall clip base-value)))
(locally
(declare (type svd svd)
(type single-float sum))
(loop for sv across svd do
(setf sum (funcall clip (+ sum
(* (aref (sv-left sv) row)
(aref (sv-right sv) column)))))))
sum))
;;; This is the performance critical loop. Compile it for each run.
(defmacro train/epoch (&key matrix approximation
normalization-factor clip do-matrix)
`(lambda (left right learning-rate)
(declare (type single-float-vector left right)
(type single-float learning-rate)
(inline ,clip)
(optimize (speed 3)))
(,do-matrix ((row column value index) ,matrix)
(let* ((l (aref left row))
(r (aref right column))
(err (- value (,clip (+ (aref ,approximation index)
(* l r))))))
(setf (aref right column) (+ r
(* learning-rate
(- (* err l)
(* ,normalization-factor r)))))
(setf (aref left row) (+ l
(* learning-rate
(- (* err r)
(* ,normalization-factor l)))))))))
;;; Well, this is not so critical as it is only used when starting
;;; from an existing SVD and when going from one sv to the next in
;;; training.
(defmacro add-sv-to-approximation (&key matrix clip do-matrix)
`(lambda (sv approximation)
(declare (type single-float-vector approximation)
(inline ,clip)
(optimize (speed 3)))
(let* ((sv (expand-sv sv ,matrix))
(left (sv-left sv))
(right (sv-right sv)))
(,do-matrix ((row column value index2) ,matrix)
(declare (ignorable value))
(setf (aref approximation index2)
(,clip (+ (aref approximation index2)
(* (aref left row)
(aref right column))))))
,matrix)))
(defun svd-1 (matrix &key trainer svd supervisor learning-rate0)
(declare (type svd svd))
;; We work with sparse indices to avoid having to map indices and
;; compact SV when necessary.
(let (sv0)
(flet ((supervise (svd iteration)
(multiple-value-bind (continuep parameters)
(funcall supervisor svd iteration)
(when continuep
(destructuring-bind (&key learning-rate sv)
parameters
(when sv
(setf sv0 (expand-sv sv matrix)))
(when learning-rate
(setf learning-rate0 learning-rate))))
continuep)))
(when (supervise svd nil)
(unless sv0
(setf sv0 (create-sv (height-of matrix :densep nil)
(width-of matrix :densep nil)
0.1)))
(loop for i upfrom 0 do
(funcall trainer (sv-left sv0) (sv-right sv0) learning-rate0)
(unless (supervise (append-to-svd svd (compact-sv sv0 matrix)) i)
(return)))
(compact-sv sv0 matrix)))))
(defun svd (matrix &key (svd (make-svd)) (base-approximator (constantly 0.0))
(learning-rate 0.001) (normalization-factor 0.02)
supervisor (clip 'identity))
"Approximate the single-float MATRIX with a quasi singular value
decomposition. Each SV of an SVD consists of a left and a right
vector. The sum of the outer products of the left and right vectors of
its consituent SVs is the approximation provided by an SVD. This SVD
is quasi because while the rank of the approximation is N, the
singular values are not explicit.
The sparse matrix interface must be supported on MATRIX.
BASE-APPROXIMATOR is a function of row and column. Its values are
effectively subtracted from those of MATRIX. Don't forget to supply
the same BASE-APPROXIMATOR to MAKE-SVD-APPROXIMATOR and
SVD-VALUE.
CLIP is a symbol that is fbound to a function that takes a single
single-float and returns it clamped into some valid range or leaves it
alone.
To make training fast, a new trainer function is compiled for each SVD
call using CLIP and DO-MATRIX-MACRO-NAME for MATRIX.
LEARNING-RATE controls the how much weights are drawn towards to
optimum at each step. By default the LEARNING-RATE is scaled by 1/MAE*
where MAE* is the MAE mean avarage error. To avoid normalization and
to have finer grained control one can return learning rate from the
supervisor.
The Tikhonov NORMALIZATION-FACTOR penalizes large weights.
After each iteration on a SV and also before adding a new SV
SUPERVISE-SVD is invoked on SUPERVISOR. The return value being NIL
indicates that the supervisor wants to stop. See SUPERVISE-SVD for
details."
(let* ((approximation (make-array (size-of matrix)
:element-type 'single-float))
(trainer (compile nil
(eval
(list 'train/epoch :matrix matrix
:approximation approximation
:normalization-factor normalization-factor
:clip clip
:do-matrix (do-matrix-macro-name matrix)))))
(adder (compile nil
(eval
(list 'add-sv-to-approximation
:matrix matrix :clip clip
:do-matrix (do-matrix-macro-name matrix)))))
(clip (coerce clip 'function)))
(map-matrix
(lambda (row column value i)
(declare (ignore value))
(setf (aref approximation i)
(funcall clip (funcall base-approximator row column))))
matrix)
(flet ((supervise (svd i)
(supervise-svd supervisor svd i
:base-approximator base-approximator
:clip clip :matrix matrix
:approximation approximation))
(add (sv)
(funcall adder sv approximation)))
(loop for sv across svd do
(add sv))
(loop for n upfrom (length svd) do
(let* ((mean-error (approximation-me matrix approximation))
(sv (svd-1 matrix :trainer trainer :svd svd
:supervisor #'supervise
:learning-rate0 (/ learning-rate
(float mean-error 0.0)))))
(unless sv
(return svd))
(add sv)
(setf svd (append-to-svd svd sv)))))))
(defun make-svd-approximator (svd &key matrix (max-n (length svd))
(base-approximator (constantly 0))
(clip #'identity))
"Return a function of (ROW COLUMN) parameters that approximates
MATRIX by SVD. The BASE-VALUE for SVD-VALUE is produced by
BASE-APPROXIMATOR for the given coordinates, while CLIP is simply
passed on. The returned function automatically translates to dense
coordinates to query the SVD."
(let ((svd (subseq svd 0 max-n)))
(lambda (row column)
(let ((base-value (funcall base-approximator row column))
(row (dense-row-index matrix row))
(column (dense-column-index matrix column)))
(svd-value svd row column :base-value base-value :clip clip)))))
;;; Utilities
(defun approximation-me (matrix dense-approximation)
(let ((sum 0.0d0)
(n 0))
(fsvd:do-matrix ((row column value index) matrix)
(declare (ignore row column))
(unless (zerop value)
(incf sum (abs (- value (aref dense-approximation index))))
(incf n)))
(if (zerop n)
1.0
(/ sum n))))
(defun approximation-rmse (matrix dense-approximation)
(let ((sum 0.0d0)
(n 0))
(fsvd:do-matrix ((row column value index) matrix)
(declare (ignore row column))
(incf sum (expt (- value (aref dense-approximation index)) 2))
(incf n))
(sqrt (/ sum n))))
(defclass limiting-supervisor ()
((svd-in-progress :initform (make-svd) :accessor svd-in-progress)
(max-n-iterations :initform nil :initarg :max-n-iterations
:accessor max-n-iterations)
(max-n-svs :initform nil :initarg :max-n-svs :accessor max-n-svs)
(trace-stream :initform *trace-output* :initarg :trace-stream
:accessor trace-stream))
(:documentation "Construct an instance, keep it around and while the
SVD is in progress inspect/save SVD-IN-PROGRESS, or set MAX-N-SVS as
you see how the learning is going."))
(defgeneric supervise-svd (supervisor svd iteration &key base-approximator clip
matrix approximation
&allow-other-keys)
(:method ((supervisor function) svd iteration &rest args)
(apply supervisor svd iteration args))
(:method ((supervisor symbol) svd iteration &rest args)
(apply supervisor svd iteration args))
(:method ((supervisor limiting-supervisor) svd iteration &key
base-approximator clip matrix approximation)
(declare (ignore base-approximator clip))
(with-slots (svd-in-progress max-n-iterations max-n-svs trace-stream)
supervisor
(when (null iteration)
(format trace-stream "With ~S SVs, RMSE: ~S~%" (length svd)
(approximation-rmse matrix approximation))
(force-output))
(setf svd-in-progress svd)
(and (or (null max-n-svs)
(<= (+ (length svd) (if iteration 0 1)) max-n-svs))
(or (null iteration) (null max-n-iterations)
(< iteration max-n-iterations)))))
(:documentation "This is invoked from SVD on its SUPERVISOR
argument. If ITERATION is NIL then a new SV is about to be added and
upon rejecting that and returning NIL the decomposition is finished.
When ITERATION is not NIL, it is a non-negative integer that is the
index of the current iteration on the last SV of SVD. MATRIX,
BASE-APPROXIMATOR, CLIP are passed through verbatim from the SVD call.
APPROXIMATION is a single-float vector that parallels MATRIX with
dense indices (see MAP-MATRIX). APPROXIMATION is updated when about to
start on a new SV.
Its second return value is a list conforming to (&KEY LEARNING-RATE
SV) that can be used to change the learning rate dynamically and to
initialize or change the compact representation of the current SV."))
;;; Simplistic implementation of the FSVD matrix interface for 2D arrays
(defmethod height-of ((array array) &key densep)
(declare (ignore densep))
(array-dimension array 0))
(defmethod width-of ((array array) &key densep)
(declare (ignore densep))
(array-dimension array 1))
(defmethod size-of ((array array))
(array-total-size array))
(defmethod map-matrix (function (array array))
(loop for row below (height-of array) do
(loop for column below (width-of array) do
(when (aref array row column)
(funcall function row column (aref array row column)
(array-row-major-index array row column))))))
(defun declarationp (form)
(and (listp form)
(eq 'declare (first form))))
(defun split-body (body)
"Return the declarations and the rest of the body as separate lists."
(let ((pos (position-if-not #'declarationp body)))
(values (subseq body 0 pos)
(subseq body pos))))
(defmacro do-array-matrix (((row column value dense-index) matrix)
&body body)
(let ((width (gensym))
(%matrix (gensym)))
(multiple-value-bind (declarations body) (split-body body)
`(let* ((,%matrix ,matrix)
(,dense-index 0)
(,width (the fixnum (width-of ,%matrix))))
(declare (type (integer 0 #.(1- most-positive-fixnum)) ,dense-index))
(dotimes (,row (the fixnum (height-of ,%matrix)))
(dotimes (,column ,width)
(let ((,value (aref ,%matrix ,row ,column)))
,@declarations
(when ,value
,@body)
(incf ,dense-index))))))))
(defmethod do-matrix-macro-name ((array array))
'do-array-matrix)