September 5th, 2010

satyr

Аннонс: cl-bpnet

Оформил "на вынос" свой очередной проект: 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))))))

И проверяем:


Как видно из рисунка, практически один в один, только на краях децл врет :)

Что ж, по-идее, для пользователя должно быть удобно, красиво и гигиенично. Теперь, что касается собственно исходного кода библиотеки.

Как бы так сказать: я не особо рекомендую, туда заглядывать =) Основные причины такой каши там две:

Ну как то так, пожалуй. Соответственно, не стесняйтесь писать комментарии, я их очень люблю =)