Оформил "на вынос" свой очередной проект:
cl-bpnet.
Это нейросеть (многослойный перцептрон) с обучением через метод обратного распространения ошибки. По возможностям сети там ничего сверхординарного нет, основные ключевые фишки проекта следующие:
- Разумеется, мощный и удобный DSL для описания сети и алгоритма ее обучения.
- Основной упор на быстродействие результирующего кода (оптимизируем все, что только можно).
- Полезная (!) многопоточность (а не то бессмысленное говно на мьютексах, что обычно встречается в библиотеках).
- Сериализация/десериализация сетей. Ну это понятно.
На официальной страничке я там пытаюсь вести
документацию в ихнем вики, основное там вроде описано, остальное потихоньку доделаем.
В
директории с примерами есть стандартная XOR-сеть, аппроксиматор функции и стенд для сравнения производительности в однопоточном и многопоточном режимах.
Давайте для затравки попробую продемонстрировать все добро на задаче аппроксимации функции синуса (
исходник полностью).
(defnet sin
(layout (inputs 1)
(outputs 1)
(layers 5)
(learning-rate 0.1)
(momentum 0.4)
(options gen-run-proc)))
Дословно, здесь определяется многослойный перцептрон с одним входом, одним выходом и одним скрытым слоем из пяти элементов; задаются коэффициенты и форсируется генерация упрощенной функции для запуска сети (прямого прохода). Подробней про defnet DSL можно почитать в
доке. В результате компиляции у нас в зубах должен оказаться класс net-sin и сгенерироваться специализация методов для него.
Теперь нужно задать алгоритм обучения. С источником примеров проблем нет (у нас же есть оригинальная функция sin), и обучить сеть достаточно на отрезке [0, pi/2] (за счет цикличности синуса).
(defnet-sin-train-method ()
(train/loop (train/before-propagate
(let* ((x (random (/ pi 2)))
(fx (sin x)))
(declare (type double-float x)
(type double-float fx))
(setf (train/input 0) (coerce x 'single-float))
(setf (train/expect 0) (coerce fx 'single-float))))
(train/before-backpropagate
(summing train/avg-sq-error into (the single-float epoch-sum-error))
(when (zerop (mod (1+ train/count) 10000))
(when (or (< epoch-sum-error 1.0)
(>= train/count 10000000))
(format t "Cycle: ~a, current sum error for 10000 cycles: ~a~%" train/count epoch-sum-error)
(leave))
(setf epoch-sum-error 0.0)))))
В DSL defnet-sin-train-method (заметим, что этот макрос был сгенерирован после описания defnet) собственно цикл обучения начинается c сегмента (train/loop &rest clauses). Здесь следует уточнить, что train/loop DSL определен поверх iterate, поэтому весь его синтаксис из train/loop свободно доступен. В нашем примере я воспользовался синтаксисом (summing ...) и оператором (leave).
Специально здесь описывать синтаксис языка train/loop не буду (но это можно посмотреть опять же в доке), я надеюсь, что должно получиться и так более или менее интуитивно понятно :) В этом же и должен быть смысл DSL-я.
На этом собственно все по функциональности аппроксиматора. Теперь можно экспериментировать:
NET-SIN> (defvar *my-net* (make-instance 'net-sin))
*MY-NET*
NET-SIN> (reset *my-net*)
NIL
NET-SIN> (train *my-net*)
Cycle: 1109999, current sum error for 10000 cycles: 0.98946446
NIL
NET-SIN> (funcall (get-run-proc *my-net*) (coerce (/ pi 6) 'single-float))
0.4991352
NET-SIN> (sin (/ pi 6))
0.49999999999999994d0
Круто, оно работает! Давайте глянем, насколько точно получилось аппроксимировать. Для этого нарисуем рядом графики обычного синуса и нашего аппроксиматора (полный код
net-sin-visualize.lisp):
(defun visualize (&key (start (* pi -2)) (end (* pi 2)))
(let ((net (make-instance 'net-sin)))
;; train the network
(reset net)
(train net)
;; visualize the difference
(call-with-cairo-context
(let ((sin-fun (make-sin-drawing-proc start end :rgb '(0.3 0 0) :line-width 1))
(sin/approx-fun (make-sin/approx-drawing-proc net start end :rgb '(0 0.3 0) :line-width 2)))
(lambda (w h)
(funcall sin-fun w h)
(funcall sin/approx-fun w h))))))
И проверяем:
Как видно из рисунка, практически один в один, только на краях децл врет :)
Что ж, по-идее, для пользователя должно быть удобно, красиво и гигиенично. Теперь, что касается собственно исходного кода библиотеки.
Как бы так сказать: я не особо рекомендую, туда заглядывать =) Основные причины такой каши там две:
- Я, возможно, немного перестарался с оптимизацией генерируемого кода.
- У меня там макросы, генерирующие макросы которые генерируют макросы... Признаюсь честно: я тупо не понимаю, как работает nested backquote :) Что, тем не менее, не мешает его мне использовать. Чисто методом тыка :)
Ну как то так, пожалуй. Соответственно, не стесняйтесь писать комментарии, я их очень люблю =)