Эффективная (параллелизуемая) и простая реализация многослойных нейронных сетей на Haskell
7 мая 2014
В общем, начитавшись Хайкина, у меня стали чесаться лапки поделать что-нить интересненькое с нейронными сетями. Писать, понятное дело, при этом я собирался на Haskell. Беглый поиск по Hackage выявил наличие множества библиотек для работы с нейронными сетями, из которых instinct и HaskellNN не только неплохо выглядели, но и устанавливались. Однако у этих библиотек есть большой недостаток (помимо фатального), заключающийся в том, что они не способны использовать всю мощь современных многоядерных процессоров за счет параллелизма. Что было дальше, вы уже и сами поняли :)
Не буду грузить вас объяснением того, что такое многослойные нейронные сети и как работает алгоритм обратного распространения ошибки. Это довольно большая тема. К тому же, вы без труда найдете массу статей по ней, да и у Хайкина прочитаете. Не стану приводить описание интерфейса моей библиотеки, потому что с ним вы можете ознакомиться благодаря документации на Hackage.
Приведу, пожалуй, простенький пример:
import Text.Printf
import System.Random
import Control.Monad
calcXor net x y =
let [r] = runNeuralNetwork net [x, y]
in r
mse net =
let square x = x * x
e1 = square $ calcXor net 0 0 - 0
e2 = square $ calcXor net 1 0 - 1
e3 = square $ calcXor net 0 1 - 1
e4 = square $ calcXor net 1 1 - 0
in 0.5 * (e1 + e2 + e3 + e4)
stopf best gnum = do
let e = mse best
when (gnum `rem` 100 == 0) $
printf "Generation: %02d, MSE: %.4f\n" gnum e
return $ e < 0.002 || gnum >= 10000
main = do
gen <- newStdGen
let af = Logistic
(rn, _) = randomNeuralNetwork gen [2,2,1] [af, af] 0.45
examples = [([0,0],[0]), ([0,1],[1]), ([1,0],[1]), ([1,1],[0])]
net <- backpropagationBatchParallel rn examples 0.4 stopf
putStrLn ""
putStrLn $ "Result: " ++ show net
printf "0 xor 0 = %.4f\n" (calcXor net 0 0)
printf "1 xor 0 = %.4f\n" (calcXor net 1 0)
printf "0 xor 1 = %.4f\n" (calcXor net 0 1)
printf "1 xor 1 = %.4f\n" (calcXor net 1 1)
Здесь происходит следующее. Случайным образом генерируется нейронная сеть (randomNeuralNetwork), состоящая из одного входного, одного скрытого и одного выходного слоя. У входного и скрытого слоя по два нейрона, у выходного — один. В слоях используется логистическая функция активации. Весам нейронной сети присваиваются случайные числа от -0.45 до 0.45.
Затем на четырех примерах прогоняется алгоритм обратного распространения ошибки в пакетном режиме со скоростью обучения 0.4. В итоге получается нейронная сеть, вычисляющая функцию XOR.
Напоминаю, что алгоритм обратного распространения ошибки имеет три режима — онлайн, стохастический и пакетный. В онлайн режиме сети приходят примеры один за другим. При получении очередного примера сеть обучается на нем, происходит обновление весов. Затем обрабатывается следующий пример. В стохастическом режиме алгоритм работает с пачкой примеров. Сеть обучается на этих примерах, веса правятся сразу после обработки каждого отдельного примера. Затем пачка перемешивается и обучение повторяется сначала. В пакетном (batch) режиме сеть прогоняется на всех примерах, затем один-единственный раз меняются веса и обучение повторяется сначала.
Моя библиотечка «из коробки» предоставляет функции для обучения нейросети в стохастическом и пакетном режимах. За счет многоядерности в силу понятных причин выигрывает только последний. При решении более сложной задачи на четырехядерном процессоре Intel Core i7-3770 3.40GHz я видел ускорение алгоритма обучения в 3.2 раза. С помощью функций, экспортируемых пакетом, вы можете реализовать какие угодно дополнительные режимы обучения.
Также библиотека предоставляет функции мутации и скрещивания нейронных сетей, используя явное кодирование. Таким образом, вы можете обучать свою нейросеть, воспользовавшись моей библиотекой для работы с генетическими алгоритмами. Однако во время работы над библиотекой я обнаружил, что явное кодирование хорошо работает только на крохотных нейронных сетях. Поэтому данная возможность, по всей видимости, имеет малую практическую ценность.
Экспериментируя с генетическими алгоритмами, я оптимизировал библиотеку в плане объема памяти, необходимого для хранения нейронной сети. Поэтому не удивляйтесь, когда найдете в коде всякие странные вещи с Word64 и битовыми операциями. В действительности, в следующих версиях библиотеки я полон решимости сделать еще один шаг в этом направлении и перейти на IntMap. Еще из планов на будущее — реализовать параллельное вычисление, а не только обучение. А также оптимизировать алгоритм обучения. Сейчас библиотечка при обратном проходе честно вычисляет производные от функций активации, в то время, как их можно вычислить на основе выходов нейронной сети, полученных при прямом проходе.
В общем-то, это все, о чем я хотел сегодня поведать. Как уже отмечалось, библиотечка лежит на Hackage. Репозиторий находится на GitHub, буду рад вашим пуллреквестам и багрепортам. Если во время чтения заметки у вас возникли вопросы, не стесняйтесь задать их в комментариях. Я с радостью на них отвечу.
Метки: Haskell, Алгоритмы, Искусственный интеллект, Параллелизм и многопоточность, Функциональное программирование.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.