Метод Монте-Карло в алгоритме обратного распространения ошибок с параллельными вычислениями

в 7:15, , рубрики: Алгоритмы, алгоритмы машинного обучения, нейросети, обратное распространение

Был проведён эксперимент для проверки, можно ли существенно уменьшить объём вычислений в алгоритме обратного распространения ошибок с параллельными вычислениями за счёт использования на каждом шаге обучения только части обучающих образцов, выбранных случайным образом, а также определение того, какой выигрыш по времени даст использование языка Ассемблера в самых внутренних циклах (в программе, написанной на языке C++).

За основу был взят классический персептрон и алгоритм обратного распространения ошибок, основанный на методе градиента, который объяснялся на курсе Mashine Learning Стэнфордского университета. Он был доработан, чтобы можно было использовать параллельные вычисления. Была написана программа на языке C++ для Linux, её функции (создание, обучение нейронной сети, распознавание данных, закачка больших файлов на сервер и т. п.) вызываются из программ, написанных на любых языках программирования, по протоколу Socket.

Для параллельных вычислений создаётся ntheads объектов нейронной сети, где ntheads — количество потоков (процессоров), в которые записываются части большого массива обучающих образцов, и на каждом шаге алгоритма обратного распространения ошибок совершается прямое и обратное распространение для каждого образца, имеющегося у объекта нейронной сети. Вычисления для каждого объекта производятся в отдельном потоке. Результатом этих вычислений являются суммарные градиенты слоёв сети каждого объекта, они суммируются друг с другом, и полученные градиенты используются для модификации матриц весов нейронной сети, которые затем прописываются во все слои сети объектов нейронной сети.

Для ускорения обучения нейронной сети обучающие образцы тасуются. В каждом объекте нейронной сети обучающие образцы делятся на pqnt_blocks блоков и на каждом шаге алгоритма вычисления производятся не по всем обучающим образцам, а только по одному блоку, на следующем шаге берется следующий по порядку блок, и т. д. (во всех объектах нейронной сети). После достижения последнего блока берётся первый, и всё циклически повторяется. За счёт этого вычисления ускоряются в pqnt_blocks раз при том же количестве итераций, но благодаря тасованию обучающих образцов качество обучения даже при pqnt_blocks, равном 10 или 20, получается вполне нормальным.

Тасование обучающих образцов

Был использован алгоритм тасования Фишера — Йетса, доработанный Ричардом Дурштенфельдом (Richard Durstenfeld) для использования в компьютерах.

    Для тасования массива A из n элементов (индекс начинается с 0):

  для всех i от n − 1 до 1 выполнить

       j ← случайное число 0 ≤ j ≤ i

       обменять местами A[j] и A[i]

Алгоритм

Рис. 1. Пример персептрона.

Рис. 1. Пример персептрона.

На рисунке показана нейронная сеть с 5 слоями, 1 слой — это входные переменные X, 5 слой — выходы сети. В программе реализована нейронная сеть с любым количеством слоёв. Для понимания, как нейронная сеть реализована в программе, привожу параметры конструктора нейронной сети:

int n, int nlayers, int pqnt, int pqnt_blocks, int nthreads, double rand_epsilon, int* nout, int tracebufcnt, char* tracebuffer, int opt

n – число входных переменных, nlayers — число слоёв, pqnt — количество обучающих образцов, pqnt_blocks — количество блоков, на которые делится массив обучающих образцов в объектах нейронных сетей каждого потока, rand_epsilon — константа для начальной инициализации матриц весов (есть значение по умолчанию), nout — массив размерностей выходов слоёв нейронной сети, tracebuffer и tracebufcnt — буфер трассировки и его размер в байтах, opt — константа, указывающая, нужно ли использовать ассемблеровский код или только код на языке C++.

Этот объект создаёт список объектов слоёв нейронной сети и аналогичные объекты для каждого потока.

Алгоритм обучения выглядит таким образом:

  1. Для iter от 0 до maxiter -1 (цикл по количеству итераций)

  2. pqnt_block_no = 0 (номер блока массива образцов)

  3. Для всех потоков выполнить расчёт суммарного градиента для каждого слоя для текущего блока массива образцов pqnt_block_no.

  4. Просуммировать градиенты каждого слоя для всех потоков, разделить полученные градиенты на количество использованных в расчётах образцов и посчитать новые значения матриц весов для каждого слоя.

  5. Записать матрицы весов каждого слоя в нейронные сети всех потоков.

  6. Увеличить pqnt_block_no на 1, если он не меньше pqnt_blocks, установить его равным 0.

  7. Конец цикла по итерациям.

Метод вычисления градиента с помощью прямого и обратного распространения объяснять особого смысла нет, поскольку всё это хорошо объясняется во множестве книг и публикаций в Интернете.

Результаты

Программа написана довольно эффективно, но было решено вычисления в самом внутреннем цикле, которые могут оказать большое влияние на общее время расчёта, написать на языке Ассемблера. Сейчас можно делать обучение нейронной сети с помощью только C++ кода и с использованием ассемблера NASM для архитектуры Intel x86, язык Ассемблера ускоряет вычисления примерно в 4 раза, язык Ассемблера для микропроцессоров Intel Xeon с использованием векторных расширений пока не использовался, но вполне понятно, что вообще в языке Ассемблера смысл есть, если не важны проблемы совместимости.

Программа на языке Octave из курса Стэнфордского университета, которую нужно было дорабатывать на этом курсе, использовала метод сопряжённых градиентов (в лекциях он не объяснялся), на моём ноутбуке она училась на 5000 образцах, состоявших из 400 чисел double, и аналогичном массиве выходов нейронной сети (их было 10), в течении примерно 16 часов. Сеть состояла из 3 слоёв, считая входные переменные, размерности матриц весов 400x25 и 25x10. Моя программа на этих образцах с такой же архитектурой нейронной сети на 4 ядрах процессора Intel Core I3 училась около 3 минут (с использованием языка Ассемблера), без тасования массива обучающих образцов и его деления на блоки, делалось 500 итераций. Эта программа была написана для Windows.

Версия для Linux (использовалась ОС Debian) с делением тасованных образцов на 10 блоков на таких же исходных данных делает расчёт за 12 секунд, с использованием языка Ассемблера — за 3 секунды. Использовался более мощный компьютер с процессором Intel Core I7 с 4 ядрами. На ноутбуке с процессором Intel Core I3 должно быть порядка 18 секунд (с использованием языка Ассемблера).

Использованные для обучения обучающие образцы также использовались для распознавания на уже обученной нейронной сети. При 500 итерациях при обучении нейронной сети с 1 блоком правильно распознавалось 94.96% образцов, с 5 блоками — 95.18% образцов, с 10 блоками -95.06% образцов, т. е. помимо ускорения вычислений деление образцов на блоки даже немного улучшает точность. При увеличении количества итераций до 1000 результаты получились такие: при обучении нейронной сети с 1 блоком правильно распознавалось 96.94% образцов, с 5 блоками — 97.1% образцов, с 10 блоками — 96.94% образцов. Всё это говорит о том, что метод градиента в такой модификации более хорошее решение давать не будет, для этого нужно увеличивать количество итераций, но деление массива обучающих образцов на блоки до определённых пределов качество обучения как минимум не снижает, и можно таким методом значительно ускорить вычисления, если самая главная задача — быстродействие.

Автор: Lakedemon

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js