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

7 мая 2014

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

Метки: , , , , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.