← На главную

Эффективная (параллелизуемая) реализация многослойных нейронных сетей на Haskell

В общем, начитавшись Хайкина, у меня стали чесаться лапки поделать что-нить интересненькое с нейронными сетями. Писать, понятное дело, при этом я собирался на Haskell. Беглый поиск по Hackage выявил наличие множества библиотек для работы с нейронными сетями, из которых instinct и HaskellNN не только неплохо выглядели, но и устанавливались. Однако у этих библиотек есть большой недостаток (помимо фатального), заключающийся в том, что они не способны использовать всю мощь современных многоядерных процессоров за счет параллелизма. Что было дальше, вы уже и сами поняли :)

Не буду грузить вас объяснением того, что такое многослойные нейронные сети и как работает алгоритм обратного распространения ошибки. Это довольно большая тема. К тому же, вы без труда найдете массу статей по ней, да и у Хайкина прочитаете. Не стану приводить описание интерфейса моей библиотеки, потому что с ним вы можете ознакомиться благодаря документации на Hackage.

Приведу, пожалуй, простенький пример:

import AI.NeuralNetworks.Simple 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, буду рад вашим пуллреквестам и багрепортам. Если во время чтения заметки у вас возникли вопросы, не стесняйтесь задать их в комментариях. Я с радостью на них отвечу.