Практики реализации нейронных сетей
При глубоком обучении иногда можно столкнуться с ситуацией, когда набор данных имеет ограниченный размер. Но чтобы получить лучшие результаты обобщение модели, необходимо иметь больше данных, в том числе и различные их вариации. То есть необходимо увеличить размер исходного набора искусственным образом, и это можно сделать с помощью аугментации данных.
| Определение: |
| Аугментация данных (англ. data augmentation) — это методика создания дополнительных данных из имеющихся данных. |
Чаще всего, проблема ограниченного набора данных возникает при решении задач, связанных с обработкой изображений. Следующие способы аугментации изображений являются самыми популярными:
![]()
Рис 1. a) Исходное изображение. b) Отображение по горизонтали. c) Вращение. d) Отступ.
e) Увеличение яркости и контрастности. f) Вырезание.
g) Добавление шума. h) Изменение RGB каналов.
- Отображение по вертикали или горизонтали (англ. flipping).
- Поворот изображения на определенный угол (англ. rotation).
- Создание отступа (англ. padding).
- Вырезание части изображения (англ. cropping).
- Добавление шума (англ. adding noise).
- Манипуляции с цветом (англ. color jittering).
Также, можно применять различные комбинации, к примеру, вырезать часть изображения, повернуть его и изменить цвет фона.
Продвинутыми способами аугментации данных является семейство порождающих состязательных сетей.
Дропаут
Одной из проблем глубокого обучения нейронных сетей является переобучение. И метод дропаут — популярный способ решения этой проблемы, благодаря простому алгоритму и хорошим практическим результатам.
| Определение: |
| Дропаут (англ. dropout) — метод регуляризации нейронной сети для предотвращения переобучения. |
В обычной нейронной сети явление переобучения появляется из-за так называемой совместной адаптации (англ. co-adaptation), то есть при обновлении весов нейрона, во время обучения методом обратного распространения ошибки, учитывается деятельность остальных нейронов с целью минимизировать функцию потерь. Поэтому веса нейронов могут меняться, исправляя при этом ошибки других нейронов. Метод дропаута как раз предотвращает эту адаптацию.
![]()
Рис 2. a) Стандартная нейронная сеть.
b) Нейронная сеть после применения дропаута.
Алгоритм дропаут
Рассмотрим слой нейронной сети состоящий из [math]H[/math] нейронов. Метод дропаут выключает нейрон с вероятностью [math]p[/math] , соответственно, оставляет включенным с вероятностью [math]q = 1 — p[/math] , причем вероятность выключения любого нейрона сети одинакова.
Пусть [math]a(x)[/math] — функция активации, тогда применение дропаута для [math]i[/math] -ого нейрона выглядит так: [math]U_ = \Theta_a(\sum\limits^_ w_x_ + b)[/math] ,
где вероятность [math]P(\Theta_[/math] [math]=[/math] [math]0)[/math] [math]= p[/math] .
Данная формула применяется на этапе обучения модели. Но так как на этом этапе нейрон остается в сети с вероятностью [math]q[/math] , на этапе тестирования необходимо эмулировать поведение нейронной сети, использованного при обучении. Для этого результат выходного значения функции активации умножается на коэффициент [math]q[/math] , то есть на этапе тестирования: [math]U_ = qa(\sum\limits^_ w_x_ + b)[/math] .
Обратный дропаут
Обратный дропаут (англ. inverted dropout) отличается от обычного тем, что умножение на коэффициент происходит на этапе обучения, причем этот коэффициент равен обратной вероятности того, что нейрон останется в сети: [math]\dfrac1[/math] . А на этапе тестирования выходное значение нейрона остается таким же, как и в методе обратного распространения ошибки.
Таким образом, выходное значение [math]i[/math] -ого нейрона на этапе обучения: [math]U_ = \dfrac1\Theta_a(\sum\limits^_ w_x_ + b)[/math] , на этапе тестирования: [math]U_ = a(\sum\limits^_ w_x_ + b)[/math] .
Обратная модификация дропаута на практике используется чаще обычной, потому что в ней не нужно менять каждый раз модель для проведения этапа тестирования.
Функции активации
Одним из важнейших аспектов глубокой нейронной сети являются функции активации.
| Определение: |
| Функция активации (англ. activation function) [math]a(x)[/math] определяет выходное значение нейрона в зависимости от результата взвешенной суммы входов и порогового значения. |
Рассмотрим нейрон, у которого взвешенная сумма входов: [math]z = \sum\limits_ w_x_ + b[/math] , где [math]w_[/math] и [math]x_[/math] — вес и входное значение [math]i[/math] -ого входа, а [math]b[/math] — смещение. Полученный результат передается в функцию активации, которая решает рассматривать этот нейрон как активированный, или его можно игнорировать.
![]()
Рис 3. Ступенчатая функция
Ступенчатая функция
Ступенчатая функция (англ. binary step function) является пороговой функцией активации. То есть если [math]z[/math] больше или меньше некоторого значения, то нейрон становится активированным. Такая функция отлично работает для бинарной классификации. Но она не работает, когда для классификации требуется большее число нейронов и количество возможных классов больше двух.
![]()
Рис 4. Линейная функция
Линейная функция
Линейная функция (англ. linear function) представляет собой прямую линию, то есть [math]a(x) = \sum\limits_ c_x_[/math] , а это значит, что результат этой функции активации пропорционален переданному аргументу. В отличии от предыдущей функции, она позволяет получить диапазон значений на выходе, а не только бинарные 0 и 1, что решает проблему классификации с большим количеством классов. Но у линейной функции есть две основных проблемы:
- Невозможность использования метода обратного распространения ошибки. Так как в основе этого метода обучения лежит градиентный спуск, а для того чтобы его найти, нужно взять производную, которая для данной функции активации — константа и не зависит от входных значений. То есть при обновлении весов нельзя сказать улучшается ли эмпирический риск на текущем шаге или нет.
- Рассмотрим нейронную сеть с несколькими слоями с данной функцией активации. Так как для каждого слоя выходное значение линейно, то они образуют линейную комбинацию, результатом которой является линейная функция. То есть финальная функция активации на последнем слое зависит только от входных значений на первом слое. А это значит, что любое количество слоев может быть заменено всего одним слоем, и, следовательно, нет смысла создавать многослойную сеть.
Главное отличие линейной функции от остальных в том, что ее область значений не ограничена: [math](-\infty; +\infty)[/math] . Следовательно, ее нужно использовать, когда выходное значение нейрона должно [math]\in \mathbb R[/math] , а не ограниченному интервалу.
![]()
Рис 5. Сигмоидная функция
Сигмоидная функция
Сигмоидная функция (англ. sigmoid function), которую также называет логистической (англ. logistic function), является гладкой монотонно возрастающей нелинейной функцией: [math]\sigma(z) = \dfrac1>[/math] . И так как эта функция нелинейна, то ее можно использовать в нейронных сетях с множеством слоев, а также обучать эти сети методом обратного распространения ошибки. Сигмоида ограничена двумя горизонтальными асимптотами [math]y = 1[/math] и [math]y = 0[/math] , что дает нормализацию выходного значения каждого нейрона. Кроме того, для сигмоидной функции характерен гладкий градиент, который предотвращает «прыжки» при подсчете выходного значения. Помимо всего этого, у этой функции есть еще одно преимущество, для значений [math]x \gt 2[/math] и [math]x \lt -2[/math] , [math]y[/math] «прижимается» к одной из асимптот, что позволяет делать четкие предсказания классов.
Несмотря на множество сильных сторон сигмоидной функции, у нее есть значительный недостаток. Производная такой функции крайне мала во всех точках, кроме сравнительно небольшого промежутка. Это сильно усложняет процесс улучшения весов с помощью градиентного спуска. Более того, эта проблема усугубляется в случае, если модель содержит много слоев. Данная проблема называется проблемой исчезающего градиента. [1]
Что касается использования сигмоидной функции, то ее преимущество над другими — в нормализации выходного значения. Иногда, это бывает крайне необходимо. К примеру, когда итоговое значение слоя должно представлять вероятность случайной величины. Кроме того, эту функцию удобно применять при решении задачи классификации, благодаря свойству «прижимания» к асимптотам.
![]()
Рис 6. Функция гиперболического тангенса
Функция гиперболического тангенса
Функция гиперболического тангенса (англ. hyperbolic tangent) имеет вид: [math]tanh(z) = \dfrac2> — 1[/math] . Эта функция является скорректированной сигмоидной функцей [math]tanh(z) = 2 \cdot sigma(2z) — 1[/math] , то есть она сохраняет те же преимущества и недостатки, но уже для диапазона значений [math](-1; 1)[/math] .
Обычно, [math]tanh[/math] является предпочтительнее сигмоиды в случаях, когда нет необходимости в нормализации. Это происходит из-за того, что область определения данной функции активации центрирована относительно нуля, что снимает ограничение при подсчете градиента для перемещения в определенном направлении. Кроме того, производная гиперболического тангенса значительно выше вблизи нуля, давая большую амплитуду градиентному спуску, а следовательно и более быструю сходимость.
![]()
Рис 7. Функция ReLU
Функция ReLU
Rectified Linear Unit — это наиболее часто используемая функция активации при глубоком обучении. Данная функция возвращает 0, если принимает отрицательный аргумент, в случае же положительного аргумента, функция возвращает само число. То есть она может быть записана как [math]f(z)=max(0, z)[/math] . На первый взгляд может показаться, что она линейна и имеет те же проблемы что и линейная функция, но это не так и ее можно использовать в нейронных сетях с множеством слоев. Функция ReLU обладает несколькими преимущества перед сигмоидой и гиперболическим тангенсом:
- Очень быстро и просто считается производная. Для отрицательных значений — 0, для положительных — 1.
- Разреженность активации. В сетях с очень большим количеством нейронов использование сигмоидной функции или гиперболического тангенса в качестве активационный функции влечет активацию почти всех нейронов, что может сказаться на производительности обучения модели. Если же использовать ReLU, то количество включаемых нейронов станет меньше, в силу характеристик функции, и сама сеть станет легче.
У данной функции есть один недостаток, называющийся проблемой умирающего ReLU [2] . Так как часть производной функции равна нулю, то и градиент для нее будет нулевым, а то это значит, что веса не будут изменяться во время спуска и нейронная сеть перестанет обучаться.
Функцию активации ReLU следует использовать, если нет особых требований для выходного значения нейрона, вроде неограниченной области определения. Но если после обучения модели результаты получились не оптимальные, то стоит перейти к другим функциям, которые могут дать лучший результат.
![]()
Рис 8. Функция Leaky ReLU
Функция Leaky ReLU
Одной из проблем стандартного ReLU является затухающий, а именно нулевой, градиент при отрицательных значениях. При использовании обычного ReLU некоторые нейроны умирают, а отследить умирание нейронов не просто. Чтобы решить эту проблему иногда используется подход ReLU с «утечкой» (leak) — график функции активации на отрицательных значениях образует не горизонтальную прямую, а наклонную, с маленьким угловым коэффициентом (порядка 0,01). То есть она может быть записана как [math]\begin f(x) = \begin 0.01x, & \text\ x \lt 0 \\ x, & \text \\ \end \end[/math] . Такое небольшое отрицательное значение помогает добиться ненулевого градиента при отрицательных значениях. Однако, функция Leaky ReLU имеет некоторые недостатки:
- Сложнее считать производную, по сравнению со стандартным подходом (так как значения уже не равны нулю), что замедляет работу каждой эпохи.
- Угловой коэффициент прямой также является гиперпараметром, который надо настраивать.
- На практике, результат не всегда сильно улучшается относительно ReLU.
Стоит отметить, что помимо проблемы умирающих нейронов, у ReLU есть и другая — проблема затухающего градиента [на 03.01.20 не создан] . При слишком большом количестве слоев градиент будет принимать очень маленькое значение, постепенно уменьшаясь до нуля. Из-за этого нейронная сеть работает нестабильно и неправильно. Leaky ReLU (LReLU) решает первую проблему, но в по-настоящему глубоких сетях проблема затухания градиента все еще встречается и при использовании этого подхода.
На практике LReLU используется не так часто. Практический результат использования LReLU вместо ReLU отличается не слишком сильно. Однако в случае использования Leaky требуется дополнительно настраивать гиперпараметр (уровень наклона при отрицательных значениях), что требует определенных усилий. Еще одной проблемой является то, что результат LReLU не всегда лучше чем при использовании обычного ReLU, поэтому чаще всего такой подход используют как альтернатива. Довольно часто на практике используется PReLU (Parametric ReLU), который позволяет добиться более значительных улучшений по сравнению с ReLU и LReLU. Также, в случае параметрической модификации ReLU, угол наклона не является гиперпараметром и настраивается нейросетью.
См. также
- Нейронные сети, перцептрон
- Глубокое обучение
- Настройка глубокой сети
Примечания
- ↑Vanishing gradient problem, Wikipedia
- ↑Dying ReLU problem, Wikipedia
Источники информации
- Аугментация данных
- Аугментация данных с помощью Imgaug package
- G. E. Hinton, N. Srivastava — Improving neural networks by preventing co-adaptation of feature detectors
- Wikipedia — Функция активации
Для чего нужен Dropout в машинном обучении?
Для чего используют Dropout в машинном обучении? Как это влияет на скорость обучения, выходные данные? Какие есть примеры практического применения Dropout? В каких случаях его нужно использовать?
Отслеживать
51.6k 200 200 золотых знаков 59 59 серебряных знаков 242 242 бронзовых знака
задан 4 авг 2021 в 11:29
Александр Рассказчиков Александр Рассказчиков
1,353 3 3 серебряных знака 17 17 бронзовых знаков
2 ответа 2
Сортировка: Сброс на вариант по умолчанию
Dropout — один из методов регуляризации (борьбы с переобучением модели). Суть dropout в «забывании» части информации. Т.е. некоторый, заданный заранее процент нейронных связей, разрывается (забывается) на выходе из текущего слоя нейронной сети. Таким образом вместо того, чтобы идеально подобрать веса только лишь для обучающего набора данных, НС учится подбирать ответ для похожих данных, которые не встречались в обучающем наборе. Это может значительно улучшить точность сети для данных, которые не встречались в обучающем наборе.
При применении dropout , обучение может длиться дольше.
Отслеживать
ответ дан 4 авг 2021 в 11:52
MaxU — stand with Ukraine MaxU — stand with Ukraine
149k 12 12 золотых знаков 59 59 серебряных знаков 132 132 бронзовых знака
Большинство ссылок гугла выдаёт определение вроде такого:
Исключение или дропаут (от англ. dropout) — метод регуляризации искусственных нейронных сетей, предназначен для уменьшения переобучения сети за счет предотвращения сложных коадаптаций отдельных нейронов на тренировочных данных во время обучения.
Использовать его нужно, если вы видите, что происходит переобучение — на тренировочной выборке сеть хорошо учится, а на тестовых/отложенных данных показывает плохой скор. Чтобы бороться с этим эффектом применяют в частности дропаут. Модель при этом хуже учится на тренировочных данных, но зато показывает лучше результат на тестовых/отложенных данных, чем был без дропаута.
Обучение при этом может идти медленнее, но нужно помнить, что на скорость обучения влияет множество параметров, и если вы меняете один параметр, вероятно нужно будет менять и другие, и как всё это вместе скажется на скорости и качестве обучения заранее обычно нельзя сказать.
Насчёт примеров конкретных не приведу, но если данных много и в них нет чёткого однозначного сигнала из-за чего модель переобучается на часть данных и плохо генерализует общую совокупность, в этом случае применяют дропаут.
Dropout — метод решения проблемы переобучения в нейронных сетях

Переобучение (overfitting) — одна из проблем глубоких нейронных сетей (Deep Neural Networks, DNN), состоящая в следующем: модель хорошо объясняет только примеры из обучающей выборки, адаптируясь к обучающим примерам, вместо того чтобы учиться классифицировать примеры, не участвовавшие в обучении (теряя способность к обобщению). За последние годы было предложено множество решений проблемы переобучения, но одно из них превзошло все остальные, благодаря своей простоте и прекрасным практическим результатам; это решение — Dropout (в русскоязычных источниках — “метод прореживания”, “метод исключения” или просто “дропаут”).
Dropout

Графическое представление метода Dropout, взятое из статьи, в которой он впервые был представлен. Слева — нейронная сеть до того, как к ней применили Dropout, справа — та же сеть после Dropout.
Сеть, изображенная слева, используется при тестировании, после обучения параметрам.
Главная идея Dropout — вместо обучения одной DNN обучить ансамбль нескольких DNN, а затем усреднить полученные результаты.
Сети для обучения получаются с помощью исключения из сети (dropping out) нейронов с вероятностью , таким образом, вероятность того, что нейрон останется в сети, составляет . “Исключение” нейрона означает, что при любых входных данных или параметрах он возвращает 0.
Исключенные нейроны не вносят свой вклад в процесс обучения ни на одном из этапов алгоритма обратного распространения ошибки (backpropagation); поэтому исключение хотя бы одного из нейронов равносильно обучению новой нейронной сети.
В стандартной нейронной сети производная, полученная каждым параметром, сообщает ему, как он должен измениться, чтобы, учитывая деятельность остальных блоков, минимизировать функцию конечных потерь. Поэтому блоки могут меняться, исправляя при этом ошибки других блоков. Это может привести к чрезмерной совместной адаптации (co-adaptation), что, в свою очередь, приводит к переобучению, поскольку эти совместные адаптации невозможно обобщить на данные, не участвовавшие в обучении. Мы выдвигаем гипотезу, что Dropout предотвращает совместную адаптацию для каждого скрытого блока, делая присутствие других скрытых блоков ненадежным. Поэтому скрытый блок не может полагаться на другие блоки в исправлении собственных ошибок.
В двух словах, Dropout хорошо работает на практике, потому что предотвращает взаимоадаптацию нейронов на этапе обучения.
Получив приблизительное представление о методе Dropout, давайте рассмотрим его подробнее.
Как работает Dropout
Как уже говорилось, Dropout выключает нейроны с вероятностью и, как следствие, оставляет их включенными с вероятностью .
Вероятность выключения каждого нейрона одинакова. Это означает следующее:
При условии, что
- — линейная проекция входного -мерного вектора x на -мерное пространство выходных значений;
- – функция активации,
применение Dropout к данной проекции на этапе обучения можно представить как измененную функцию активации:
где – -мерный вектор случайных величин , распределенных по закону Бернулли.
имеет следующее распределение вероятностей:
где — все возможные выходные значения.
Очевидно, что эта случайная величина идеально соответствует Dropout, примененному к одному нейрону. Действительно, нейрон выключают с вероятностью , в противном случае — оставляют включенным.
Посмотрим на применение Dropout к i-му нейрону:
Так как на этапе обучения нейрон остается в сети (не подвергается выключению) с вероятностью q, на этапе тестирования нам необходимо эмулировать поведение ансамбля нейронных сетей, использованного при обучении. Для этого авторы предлагают на этапе тестирования умножить функцию активации на коэффициент q. Таким образом,
На этапе обучения:,
На этапе тестирования:
Обратный (Inverted) Dropout
Возможно использовать немного другой подход — обратный Dropout. В данном случае мы умножаем функцию активации на коэффициент не во время тестового этапа, а во время обучения.
Коэффициент равен обратной величине вероятности того, что нейрон останется в сети: , таким образом,
На этапе обучения: ,
На этапе тестирования:
Во многих фреймворках для глубокого обучения Dropout реализован как раз в этой модификации, так как при этом необходимо лишь однажды описать модель, а потом запускать обучение и тестирование на этой модели, меняя только параметр (коэффициент Dropout).
В случае прямого Dropout мы вынуждены изменять нейронную сеть для проведения тестирования, так как без умножения на q нейрон будет возвращать значения выше, чем те, которые ожидают получить последующие нейроны; именно поэтому реализация обратного Dropout встречается чаще.
Dropout множества нейронов
Легко заметить, что слой из нейронов на отдельном шаге этапа обучения можно рассматривать как ансамбль из экспериментов Бернулли с вероятностью успеха .
Таким образом, на выходе слоя мы получаем следующее количество исключенных нейронов:
Так как каждый нейрон представлен в виде случайной величины, распределенной по закону Бернулли, и все эти величины независимы, общее число исключенных нейронов — также случайная величина, но имеющая биномиальное распределение:
где вероятность успешных событий за попыток характеризуется следующей плотностью распределения:
Эту формулу легко объяснить следующим образом:
- — вероятность получения последовательности успешных событий за попыток, и, следовательно, неуспешных.
- — биномиальный коэффициент, используемый для расчета количества возможных успешных последовательностей.
Используя метод Dropout, мы фиксируем коэффициент Dropout для определенного слоя и ожидаем, что из этого слоя будет исключено пропорциональное количество нейронов.
Например, если слой, к которому мы применили Dropout, состоит из нейронов, а , мы ожидаем, что 512 из них будет отключено. Давайте проверим это утверждение:
Как видим, вероятность отключения ровно нейронов составляет всего 0.025!
Следующий скрипт на Python 3 поможет представить, сколько нейронов будет выключено для разных значений и зафиксированного количества .
import matplotlib.pyplot as plt from scipy.stats import binom import numpy as np # number of neurons n = 1024 # number of tests (input examples) size = 500 # histogram bin width, for data visualization binwidth = 5 for p in range(1, 10): # per layer probability prob = p / 10 # generate size values from a bi(n, prob) rnd_values = binom.rvs(n, prob, size=size) # draw histogram of rnd values plt.hist( rnd_values, bins=[x for x in range(0, n, binwidth)], # normalize = extract the probabilities normed=1, # pick a random color color=np.random.rand(3, 1), # label the histogram with its probability label=str(prob)) plt.legend(loc='upper right') plt.show()

Биномиальное распределение достигает максимума в районе
Как видим из изображения выше, для любого среднее количество выключенных нейронов пропорционально , то есть
Более того, можно заметить, что распределение значений почти симметрично относительно и вероятность выключения нейронов возрастает по мере того, как увеличивается расстояние от .
Коэффициент масштабирования введен авторами для компенсации значений активации, так как на этапе обучения используется лишь доля от нейронов, в то время как на этапе тестирования все 100% нейронов остаются включенными, и, следовательно, полученные значения необходимо уменьшить с помощью специального коэффициента.
Dropout и другие регуляризаторы
Dropout часто используется с L2-нормализацией и другими методами ограничения параметров (например, Max Norm). Методы нормализации помогают поддерживать невысокие значения параметров модели.
Вкратце, L2-нормализация представляет собой дополнительный элемент функции потерь, где — гиперпараметр, называемый “сила регуляризации” (regularization strength), — модель, а — функция ошибки между реальным значением и предсказанным :
Нетрудно понять, что этот дополнительный элемент сокращает величину, на которую изменяется параметр во время обратного распространения ошибки методом градиентного спуска. Если — коэффициент скорости обучения, то параметр обновляется на следующую величину:
Dropout сам по себе не может предотвратить рост значений параметров на этапе обновления. Более того, обратный Dropout ведет к тому, чтобы шаги обновления стали даже больше, как показано ниже.
Обратный Dropout и другие регуляризаторы
Так как Dropout не предотвращает роста параметров и их переполнения, нам может помочь L2-регуляризация (или любой другой метод регуляризации, ограничивающий значения параметров).
Если мы представим коэффициент Dropout в явном виде, уравнение выше превратится в следующее:
Нетрудно заметить, что в случае обратного Dropout скорость обучения масштабируется с помощью коэффициента . Так как принадлежит интервалу , отношение между и может принимать значения из следующего интервала:
Поэтому отныне мы будем называть ускоряющим множителем (boosting factor), так как он увеличивает скорость обучения. будем называть эффективной скоростью обучения (effective learning rate).
Эффективная скорость обучения выше, чем скорость обучения, выбранная нами: таким образом метода нормализации, ограничивающие значения параметров, могут упростить процесс выбора скорости обучения.
Итоги
- Dropout существует в двух модификациях: прямой (используется нечасто) и обратный.
- Dropout на отдельном нейроне может быть представлен как случайная величина с распределением Бернулли.
- Dropout на множестве нейронов может быть представлен как случайная величина с биномиальным распределением.
- Несмотря на то, что вероятность того, что из сети будет выключено ровно np нейронов, np — среднее количество нейронов, отключенных в слое из n нейронов.
- Обратный Dropout увеличивает скорость обучения.
- Обратный Dropout следует использовать совместно с другими методами нормализации, ограничивающими значения параметров, чтобы упростить процесс выбора скорости обучения.
- Dropout помогает предотвратить проблему обучения в глубоких нейронных сетях.
О, а приходите к нам работать? 🙂
wunderfund.io — молодой фонд, который занимается высокочастотной алготорговлей. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.
Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.
Присоединяйтесь к нашей команде: wunderfund.io
Нейросети — это просто (Часть 12): Dropout

От начала данного цикла статей мы уже хорошо продвинулись в изучении различных моделей нейронных сетей. Но процесс обучения всегда проходил без нашего участия. В то же время постоянно возникает желание как-то помочь нейронной сети повысить результаты ее обучения, так называемую, сходимость нейронной сети. В данной статье предлагаю рассмотреть один из таких методов — Dropout.
1. Dropout, как метод повышения сходимости нейронных сетей
При обучении нейронной сети на вход каждого нейрона подается большое количество признаков и сложно оценить влияние каждого из них. В результате, ошибки одних нейронов сглаживаются правильными значениями других, а на выходе нейронной сети ошибки накапливаются. И как результат, обучение останавливается в некоем локальном минимуме с достаточно большой ошибкой. Данный эффект был назван совместной адаптацией признаков, когда влияние каждого признака как-бы подстраивается под окружающую среду. Для нас было бы лучше получить обратный эффект, когда среда будет разложена по отдельным признакам и оценивать отдельно влияние каждого.
Для борьбы со сложной совместной адаптацией признаков в июле 2012 года группа ученных из университета Торонто предложила случайным образом исключать часть нейронов в процессе обучения [12]. Снижение количества признаков при обучении повышает значимость каждого, а постоянное изменение количественного и качественного состава признаков снижает риск их совместной адаптации. Такой метод и получил название Dropout. Некоторые сравнивают применение данного метода с деревьями решений, ведь согласитесь, исключая часть нейронов, мы на каждой итерации обучения получаем новую нейронную сеть со своими весовыми коэффициентами. А по правилам комбинаторики вариативность таких сетей довольно высока.
В процессе эксплуатации нейронной сети оцениваются все признаки и нейроны, тем самым получаем максимально точную и независимую оценку текущего состояния изучаемой среды.
Авторы решения в своей статье [12] указывают на возможность использования метода и для повышения качества предварительно обученных моделей.
Описывая, предложенное решение, с точки зрения математики можно сказать, что каждый отдельный нейрон выкидывается из процесса с некой заданной вероятностью p. Или нейрон будет участвовать в процессе обучения нейронной сети с вероятностью q=1-p.
Для определения списка исключаемых нейронов используется генератор псевдослучайных чисел с нормальным распределением. Такой подход позволяет достичь максимально-возможного равномерного исключения нейронов. На практике сгенерируем вектор размером равным входной последовательности. Для используемых признаков в векторе пропишем «1», а для исключаемых элементов поставим «0».
Однако, исключение анализируемых признаков несомненно ведет к снижению суммы на входе функции активация нейрона. Для компенсации этого эффекта умножим значение каждого признака на коэффициент 1/q. Легко заметить, что данный коэффициент будет увеличивать значения, так как вероятность q всегда будет в диапазоне от 0 до 1.

,
d — элементы вектора результатов Dropout,
q — вероятность использования нейрона в процессе обучения,
x — элементы вектора маскирования,
n — элементы входной последовательности.
При обратном проходе в процессе обучения градиент ошибки умножается на производную вышеприведенной функции. Как легко заметить, в случае Dropout обратный проход будет аналогичен прямому с использованием вектора маскирования из прямого прохода.
В процессе эксплуатации нейронной сети вектор маскирования заполняется «1», что позволяет значениям беспрепятственно передаваться в обоих направлениях.
На практике коэффициент 1/q постоянен на протяжении всего обучения, поэтому мы легко можем посчитать данный коэффициент один раз и записывать его вместо «1» в тензор маскирования. Тем самым исключим операции пересчета коэффициента и умножения его на «1» маски в каждой итерации обучения.
2. Реализация
После рассмотрения теоретических аспектов предлагаю перейти к рассмотрению вариантов реализации данного метода в нашей библиотеке. И первое, с чем мы сталкиваемся — это реализация двух различных алгоритмов. Один для процесса обучения, второй для тестирования и промышленной эксплуатации. Соответственно, нам нужно явно указать нейрону, по какому алгоритму нужно работать в каждом отдельно взятом случае. Для этого на уровне базового нейрона введем флаг bTrain, которому будем присваивать значение true в процессе обучения и false в процессе тестирования.
class CNeuronBaseOCL : public CObject < protected: bool bTrain; /// < Training Mode Flag
Для управления значениями флага создадим вспомогательные методы.
virtual void TrainMode(bool flag) < bTrain=flag; >/// < Set Training Mode Flag virtual bool TrainMode(void) < return bTrain; >/// < Get Training Mode Flag
Введение флага и методов на уровне базового нейрона сделано намерено. Это позволит нам использовать наработки dropout в последующих разработках.
2.1. Создаем новый класс для нашей модели
Для реализации алгоритма Dropout создадим новый класс CNeuronDropoutOCL, который мы будем включать в нашу модель отдельным слоем. Новый класс будет наследоваться на прямую от класса базового нейрона CNeuronBaseOCL. В блоке protected объявим переменные:
- OutProbability — заданная вероятность исключения нейронов.
- OutNumber — количество исключаемых нейронов.
- dInitValue — значение для инициализации вектора маскирования, в теоретической части этой статьи мы обозначали данный коэффициент как 1/q.
Также объявим два указателя на классы:
- DropOutMultiplier — вектор маскирования.
- PrevLayer — указатель на объект предыдущего слоя, будем использовать в процессе тестирования и промышленной эксплуатации.
class CNeuronDropoutOCL : public CNeuronBaseOCL < protected: CNeuronBaseOCL *PrevLayer; double OutProbability; double OutNumber; CBufferDouble *DropOutMultiplier; double dInitValue; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL); /// virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) return true;> /// < Method for updating weights.@param NeuronOCL Pointer to previos layer.//--- int RND(void) < xor128; return (int)((double)(Neurons()-1)/UINT_MAX*rnd_w); > /// < Generates a random neuron position to turn offpublic: CNeuronDropoutOCL(void); ~CNeuronDropoutOCL(void); //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint numNeurons,double out_prob, ENUM_OPTIMIZATION optimization_type); /// < Method of initialization class.@param[in] numOutputs Number of connections to next layer.@param[in] myIndex Index of neuron in layer.@param[in] open_cl Pointer to #COpenCLMy object. //#param[in] numNeurons Number of neurons in layer #param[in] out_prob Probability of neurons shutdown @param optimization_type Optimization type (#ENUM_OPTIMIZATION)@return Boolen result of operations.//--- virtual int getOutputIndex(void) < return (bTrain ? Output.GetIndex() : PrevLayer.getOutputIndex()); > /// < Get index of output buffer @return Indexvirtual int getGradientIndex(void) < return (bTrain ? Gradient.GetIndex() : PrevLayer.getGradientIndex()); > /// < Get index of gradient buffer @return Index//--- virtual int getOutputVal(double &values[]) < return (bTrain ? Output.GetData(values) : PrevLayer.getOutputVal(values)); > /// < Get values of output buffer @param[out] values Array of data @return number of itemsvirtual int getOutputVal(CArrayDouble *values) < return (bTrain ? Output.GetData(values) : PrevLayer.getOutputVal(values)); > /// < Get values of output buffer @param[out] values Array of data @return number of itemsvirtual int getGradient(double &values[]) < return (bTrain ? Gradient.GetData(values) : PrevLayer.getGradient(values)); > /// < Get values of gradient buffer @param[out] values Array of data @return number of itemsvirtual CBufferDouble *getOutput(void) < return (bTrain ? Output : PrevLayer.getOutput()); > /// < Get pointer of output buffer @return Pointer to objectvirtual CBufferDouble *getGradient(void) < return (bTrain ? Gradient : PrevLayer.getGradient()); > /// < Get pointer of gradient buffer @return Pointer to object//--- virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL); /// < Method to transfer gradient to previous layer by calling kernel ::CalcHiddenGradient(). @param NeuronOCL Pointer to next layer.//--- virtual bool Save(int const file_handle);/// < Save method @param[in] file_handle handle of file @return logical result of operationvirtual bool Load(int const file_handle);/// < Load method @param[in] file_handle handle of file @return logical result of operation//--- virtual int Type(void) const < return defNeuronDropoutOCL; >/// < Identificator of class.@return Type of class>;
Список методов класса довольно узнаваем и все они переопределяют методы родительского класса. Исключением из этого правила является метод RND, который предназначен для генерации псевдослучайных чисел равномерного распределения. Алгоритм этого метода позаимствован из статьи [13]. С целью получения максимально случайных значений во всех объектах нашей нейронной сети, генератор псевдослучайной последовательности реализован в виде макроподстановки с использованием глобальных переменных.
#define xor128 rnd_t=(rnd_x^(rnd_x11)); \ rnd_x=rnd_y; \ rnd_y=rnd_z; \ rnd_z=rnd_w; \ rnd_w=(rnd_w^(rnd_w>>19))^(rnd_t^(rnd_t>>8)) uint rnd_x=MathRand(), rnd_y=MathRand(), rnd_z=MathRand(), rnd_w=MathRand(), rnd_t=0;
Предложенный алгоритм генерирует последовательность целых чисел в диапазоне [0,UINT_MAX=4294967295]. Поэтому в методе генератора псевдослучайной последовательности после выполнения макроса полученное значение нормализуется до размера последовательности.
int RND(void) < xor128; return (int)((double)(Neurons()-1)/UINT_MAX*rnd_w); >
Читатель, знакомый с предыдущими публикациями данной серии статей, может обратить внимание, что раньше мы не переопределяли методы работы с буферами данных класса из других объектов. Напомню, что указанные методы используются для обмена данными между слоями нейронной сети, когда нейроны обращаются к данными предшествующего или последующего слоя.
Такое решение было принято в попытке оптимизировать работу нейронной сети в процессе промышленной эксплуатации. Повторюсь, слой Dropout используется только в процессе обучения нейронной сети. При тестировании и промышленной эксплуатации алгоритм исключения нейронов отключается. Переопределив методы обращения к буферам данных, мы организовали пропуск слоя Dropout. Все переопределенные методы построены по одному принципу. Вместо копирования данных организовываем подмену буферов слоя Dropout буферами предыдущего слоя. Таким образом, в режиме промышленной эксплуатации скорость работы нейронной сети со слоем Dropout сопоставима со скоростью работы аналогичной сети без Dropout, при этом получаем все преимущества исключений нейронов при обучении сети.
virtual int getOutputIndex(void) < return (bTrain ? Output.GetIndex() : PrevLayer.getOutputIndex()); >
С полным кодом всех методов и классов можно ознакомиться во вложении.
2.2. Прямой проход
Не нарушая традиции, прямой проход организуем в методе feedForward. В начале метода проверим действительность полученного в параметрах указателя на предыдущий слой нейронной сети и указателя на объект OpenCL. После чего сохраним функцию активации, используемую на предыдущем слое и сам указатель на объект предыдущего слоя. Для режима промышленной эксплуатации нейронной сети на этом заканчивается прямой проход слоя Dropout. Далее, при обращении от последующего слоя включится механизм подмены буферов данных, описанный выше.
bool CNeuronDropoutOCL::feedForward(CNeuronBaseOCL *NeuronOCL) < if(CheckPointer(OpenCL)==POINTER_INVALID || CheckPointer(NeuronOCL)==POINTER_INVALID) return false; //--- activation=(ENUM_ACTIVATION)NeuronOCL.Activation(); PrevLayer=NeuronOCL; if(!bTrain) return true;
Последующие итерации релевантные только для режима обучения нейронной сети. В начале сформируем вектор маскирования, в котором определим нейроны к отключению на данном шаге. Маску прописываем в буфер DropOutMultiplier, проверим наличие ранее созданного объекта и при необходимости создадим новый. Проинициализируем буфер начальными значениями. Для сокращения вычислений мы будем инициализировать буфер сразу повышающим коэффициентом 1/q.
if(CheckPointer(DropOutMultiplier)==POINTER_INVALID) DropOutMultiplier=new CBufferDouble(); if(!DropOutMultiplier.BufferInit(NeuronOCL.Neurons(),dInitValue)) return false; for(int i=0;iuint p=RND(); double val=DropOutMultiplier.At(p); if(val==0 || val==DBL_MAX) < i--; continue; > if(!DropOutMultiplier.Update(RND(),0)) return false; >
После инициализации буфера организуем цикл с числом повторений равным числу нейронов к исключению и будем заменять случайно выбранные элементы буфера нулевыми значениями. Для исключения риска дважды записать «0» в одну ячейку организуем дополнительную проверку внутри нашего цикла.
После генерации маски создадим буфер непосредственно в памяти GPU и перенесем данные.
if(!DropOutMultiplier.BufferCreate(OpenCL)) return false;
Теперь нам осталось произвести поэлементное умножение двух векторов. Результат этой операции и будет выходом слоя Dropout. Операцию умножения векторов выполним средствами OpenCL в GPU. Для умножения элементов наиболее эффективным будет использование векторных операций. Я в кернеле OpenCL использовал переменные типа double4, т. е. вектор из 4-х элементов. Поэтому число запускаемых потоков будет в 4 раза меньше количества элементов в векторах.
uint global_work_offset[1]= 0>; uint global_work_size[1]; int i=Neurons()%4; global_work_size[0]=(Neurons()-i)/4+(i>0 ? 1 : 0);
Далее укажем буферs и переменные исходных данных и запустим на выполнение кернел.
if(!OpenCL.SetArgumentBuffer(def_k_Dropout,def_k_dout_input,NeuronOCL.getOutputIndex())) return false; if(!OpenCL.SetArgumentBuffer(def_k_Dropout,def_k_dout_map,DropOutMultiplier.GetIndex())) return false; if(!OpenCL.SetArgumentBuffer(def_k_Dropout,def_k_dout_out,Output.GetIndex())) return false; if(!OpenCL.SetArgument(def_k_Dropout,def_k_dout_dimension,Neurons())) return false; ResetLastError(); if(!OpenCL.Execute(def_k_Dropout,1,global_work_offset,global_work_size)) < printf("Error of execution kernel Dropout: %d",GetLastError()); return false; >
В заключение метода получим результат выполнения операций в кернеле и удалим из памяти GPU буфер маскирования.
if(!Output.BufferRead()) return false; DropOutMultiplier.BufferFree(); //--- return true; >
После выполнения операций выходим из метода с результатом true.
Описание метода прямого прохода без рассмотрения операций на стороне GPU будет неполным. Посмотрим на код кернела.
__kernel void Dropout (__global double *inputs, /// __global double *map, /// __global double *out, /// int dimension /// < Dimension of matrix)
В параметрах кернел получает указатели на 2 входящих тензора c исходными данными и тензор результатов, а также размерность векторов.
Непосредственно в коде кернела по номеру потока определяем элементы для умножения и далее код разделяется на 2 ветки. Первая, основная: с использованием векторных операций осуществляем умножение 4-х последовательных элементов и записываем полученные данные в соответствующие элементы буфера результатов.
< const int i=get_global_id(0)*4; if(i+31],inputs[i+2],inputs[i+3])*(double4)(map[i],map[i+1],map[i+2],map[i+3]); out[i]=k.s0; out[i+1]=k.s1; out[i+2]=k.s2; out[i+3]=k.s3; > else for(int k=i;k 4);k++) out[i+k]=(inputs[i+k]*map[i+k]); >
Вторая ветка включается только в случаях, когда количество элементов в тензорах не кратно 4 и в цикле перемножаются оставшиеся элементы. Легко заметить, что в данном цикле будет не более 3-х итераций и не будет критичным по времени вычислений.
С полным кодом всех классов и их методов можно ознакомиться во вложении.
2.3. Обратный проход
Обратный проход во всех ранее рассмотренных нейронах делился на 2 метода:
- calcInputGradients — передача градиента ошибки на предыдущий слой.
- updateInputWeights — обновление весовых коэффициентов нейронного слоя.
В случае Dropout у нас отсутствует тензор весовых коэффициентов, но для поддержания общей структуры объектов мы все же переопределим метод updateInputWeights, но в данном случае он всегда будет возвращать значение true.
virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) return true;> /// < Method for updating weights.
Рассмотрим реализацию метода calcInputGradients. В параметрах он получает указатель на предшествующий слой, и в начале метода проверим действительность полученного указателя и указателя на объект OpenCL. И далее, как и при прямом проходе, делаем разделение алгоритма на процесс обучения и процесс промышленной эксплуатации. В режиме тестирования или промышленной эксплуатации на этом месте мы выходим из метода, т. к. благодаря нашей подмене буферов данных последующий нейронный слой записал градиент напрямую в буфер предшествующего слоя, миную излишние итерации в слое Dropout.
bool CNeuronDropoutOCL::calcInputGradients(CNeuronBaseOCL *NeuronOCL) < if(CheckPointer(OpenCL)==POINTER_INVALID || CheckPointer(NeuronOCL)==POINTER_INVALID) return false; //--- if(!bTrain) return true;
В режиме обучения передача градиента будет осуществляться по другому пути. И приведенный ниже алгоритм будет релевантный только в процессе обучения нейронной сети. Как и в методе прямого прохода проверим действительность указателя на буфер маскирования DropOutMultiplier. Только в отличие от прямого прохода, ошибка проверки не повлечет создание нового буфера, а приведет к выходу из метода с результатом false. Это связано с тем, что при обратном проходе используется маска, сгенерированная при прямом проходе. Такой подход обеспечивает сопоставимость данных и правильное распределение градиента ошибки между нейронами.
if(CheckPointer(DropOutMultiplier)==POINTER_INVALID) return false; //--- if(!DropOutMultiplier.BufferCreate(OpenCL)) return false;
После успешной проверки объекта DropOutMultiplier создадим буфер в памяти GPU и заполним его данными.
Теперь нам осталось произвести поэлементное умножение двух векторов . Ничего не напоминает? Точно такое предложение приведено выше при описании прямого прохода. Да, действительно. В теоретической части показано, что производная математической функции Dropout равна повышающему коэффициенту. Поэтому при обратном проходе мы также будем умножать градиент от последующего слоя на повышающий коэффициент, записанный в буфере маскирования DropOutMultiplier. Таким образом, класс CNeuronDropoutOCL является тем уникальным случаем, когда и для прямого и для обратного проходов мы будем использовать один кернел, только на вход будем подавать разные данные: при прямом проходе — выходные данные нейронов, при обратном проходе — градиент ошибки.
И так далее осуществим указание буферов данных и вызов исполнения кернела. Приведенный код аналогичен коду прямого прохода и, думаю, не требует дополнительных пояснений.
uint global_work_offset[1]= 0>; uint global_work_size[1]; int i=Neurons()%4; global_work_size[0]=(Neurons()-i)/4+(i>0 ? 1 : 0); if(!OpenCL.SetArgumentBuffer(def_k_Dropout,def_k_dout_input,Gradient.GetIndex())) return false; if(!OpenCL.SetArgumentBuffer(def_k_Dropout,def_k_dout_map,DropOutMultiplier.GetIndex())) return false; if(!OpenCL.SetArgumentBuffer(def_k_Dropout,def_k_dout_out,NeuronOCL.getGradientIndex())) return false; if(!OpenCL.SetArgument(def_k_Dropout,def_k_dout_dimension,Neurons())) return false; ResetLastError(); if(!OpenCL.Execute(def_k_Dropout,1,global_work_offset,global_work_size)) < printf("Error of execution kernel Dropout: %d",GetLastError()); return false; > if(!NeuronOCL.getGradient().BufferRead()) return false; DropOutMultiplier.BufferFree(); //--- return true; >
С полным кодом всех классов и их методов можно ознакомиться во вложении.
2.4. Методы сохранения и загрузки данных
Немного остановимся на методах сохранения и загрузки объекта нейронного слоя Dropout. Нам нет необходимости сохранять объект буфера маски, т. к. новая маска генерируется на каждом цикле обучения. В методе инициализации класса CNeuronDropoutOCL была добавлена только одна переменная — вероятность исключения нейрона, ее мы и сохраним.
Таким образом, в методе сохранения Save вызовем одноименный метод родительского класса и после успешного завершения сохраним заданную вероятность исключения нейронов.
bool CNeuronDropoutOCL::Save(const int file_handle) < if(!CNeuronBaseOCL::Save(file_handle)) return false; //--- if(FileWriteDouble(file_handle,OutProbability)0) return false; //--- return true; >
В методе загрузка класса Load нам предстоит считать данные с диска и восстановить все элементы класса, поэтому алгоритм метода будет немного сложнее метода сохранения.
Повторяя код метода сохранения класса, вызовем одноименный метод родительского класса. После его завершения считаем вероятность исключения нейронов. На этом метод сохранения завершился, но нам нужно восстановить недостающие элементы. Исходя из вероятности исключения нейронов посчитаем количество нейронов к исключению и значение повышающего коэффициента, который также служит значением для инициализации вектора маскирования.
bool CNeuronDropoutOCL::Load(const int file_handle) < if(!CNeuronBaseOCL::Load(file_handle)) return false; //--- OutProbability=FileReadDouble(file_handle); OutNumber=(int)(Neurons()*OutProbability); dInitValue=1/(1-OutProbability); if(CheckPointer(DropOutMultiplier)==POINTER_INVALID) DropOutMultiplier=new CBufferDouble(); if(!DropOutMultiplier.BufferInit(Neurons()+1,dInitValue)) return false; //--- return true; >
После проведенного расчета восстановим вектор маскирования. Проверим действительность указателя на объект буфера данных в DropOutMultiplier и, при необходимости, создадим новый объект. После чего проинициализируем вектор маскирования начальными значениями.
2.5. Точечные изменения в базовых классах нейронной сети
И как всегда, после создания нового класса нам нужно корректно вписать его в работу нашей библиотеки. Первое, что мы сделаем, это объявим макроподстановки для работы с новым кернелом, а также зададим константу идентификации нового класса.
#define def_k_Dropout 23 /// < Index of the kernel for Dropout process (#Dropout)#define def_k_dout_input 0 /// < Inputs Tensor#define def_k_dout_map 1 /// < Map Tensor#define def_k_dout_out 2 /// < Out Tensor#define def_k_dout_dimension 3 /// < Dimension of Inputs
#define defNeuronDropoutOCL 0x7890 ///
Затем, в метод описания нейронного слоя добавим новую переменную для записи вероятности исключения нейронов.
class CLayerDescription : public CObject < public: /** Constructor */ CLayerDescription(void); /** Destructor */~CLayerDescription(void) <>; //--- int type; /// < Type of neurons in layer (\ref ObjectTypes)int count; /// < Number of neuronsint window; /// < Size of input windowint window_out; /// < Size of output windowint step; /// < Step sizeint layers; /// < Layers countENUM_ACTIVATION activation; /// < Type of activation function (#ENUM_ACTIVATION)ENUM_OPTIMIZATION optimization; /// < Type of optimization method (#ENUM_OPTIMIZATION)double probability; /// < Probability of neurons shutdown, only Dropout used>;
В методе создания нейронной сети CNet::CNet в блоке создания и инициализации слоев добавим код инициализации нового слоя (ниже по коду выделено заливкой).
for(int i=0; iif((i+1)1); if(CheckPointer(next)==POINTER_INVALID) return; > else next=NULL; int outputs=(next==NULL || (next.type!=defNeuron && next.type!=defNeuronBaseOCL) ? 0 : next.count); temp=new CLayer(outputs); int neurons=(desc.count+(desc.type==defNeuron || desc.type==defNeuronBaseOCL ? 1 : 0)); if(CheckPointer(opencl)!=POINTER_INVALID) < CNeuronBaseOCL *neuron_ocl=NULL; CNeuronConvOCL *neuron_conv_ocl=NULL; CNeuronAttentionOCL *neuron_attention_ocl=NULL; CNeuronMLMHAttentionOCL *neuron_mlattention_ocl=NULL; CNeuronDropoutOCL *dropout=NULL; switch(desc.type) < case defNeuron: case defNeuronBaseOCL: neuron_ocl=new CNeuronBaseOCL(); if(CheckPointer(neuron_ocl)==POINTER_INVALID) < delete temp; return; > if(!neuron_ocl.Init(outputs,0,opencl,desc.count,desc.optimization)) < delete neuron_ocl; delete temp; return; > neuron_ocl.SetActivationFunction(desc.activation); if(!temp.Add(neuron_ocl)) < delete neuron_ocl; delete temp; return; > neuron_ocl=NULL; break; //--- case defNeuronConvOCL: neuron_conv_ocl=new CNeuronConvOCL(); if(CheckPointer(neuron_conv_ocl)==POINTER_INVALID) < delete temp; return; > if(!neuron_conv_ocl.Init(outputs,0,opencl,desc.window,desc.step,desc.window_out,desc.count,desc.optimization)) < delete neuron_conv_ocl; delete temp; return; > neuron_conv_ocl.SetActivationFunction(desc.activation); if(!temp.Add(neuron_conv_ocl)) < delete neuron_conv_ocl; delete temp; return; > neuron_conv_ocl=NULL; break; //--- case defNeuronAttentionOCL: neuron_attention_ocl=new CNeuronAttentionOCL(); if(CheckPointer(neuron_attention_ocl)==POINTER_INVALID) < delete temp; return; > if(!neuron_attention_ocl.Init(outputs,0,opencl,desc.window,desc.count,desc.optimization)) < delete neuron_attention_ocl; delete temp; return; > neuron_attention_ocl.SetActivationFunction(desc.activation); if(!temp.Add(neuron_attention_ocl)) < delete neuron_attention_ocl; delete temp; return; > neuron_attention_ocl=NULL; break; //--- case defNeuronMHAttentionOCL: neuron_attention_ocl=new CNeuronMHAttentionOCL(); if(CheckPointer(neuron_attention_ocl)==POINTER_INVALID) < delete temp; return; > if(!neuron_attention_ocl.Init(outputs,0,opencl,desc.window,desc.count,desc.optimization)) < delete neuron_attention_ocl; delete temp; return; > neuron_attention_ocl.SetActivationFunction(desc.activation); if(!temp.Add(neuron_attention_ocl)) < delete neuron_attention_ocl; delete temp; return; > neuron_attention_ocl=NULL; break; //--- case defNeuronMLMHAttentionOCL: neuron_mlattention_ocl=new CNeuronMLMHAttentionOCL(); if(CheckPointer(neuron_mlattention_ocl)==POINTER_INVALID) < delete temp; return; > if(!neuron_mlattention_ocl.Init(outputs,0,opencl,desc.window,desc.window_out,desc.step,desc.count,desc.layers,desc.optimization)) < delete neuron_mlattention_ocl; delete temp; return; > neuron_mlattention_ocl.SetActivationFunction(desc.activation); if(!temp.Add(neuron_mlattention_ocl)) < delete neuron_mlattention_ocl; delete temp; return; > neuron_mlattention_ocl=NULL; break; //--- case defNeuronDropoutOCL: dropout=new CNeuronDropoutOCL(); if(CheckPointer(dropout)==POINTER_INVALID) < delete temp; return; > if(!dropout.Init(outputs,0,opencl,desc.count,desc.probability,desc.optimization)) < delete dropout; delete temp; return; > if(!temp.Add(dropout)) < delete dropout; delete temp; return; > dropout=NULL; break; //--- default: return; break; > >
И не забудем в этом же методе объявить новый кернел.
opencl.SetKernelsCount(24); opencl.KernelCreate(def_k_FeedForward,"FeedForward"); opencl.KernelCreate(def_k_CalcOutputGradient,"CalcOutputGradient"); opencl.KernelCreate(def_k_CalcHiddenGradient,"CalcHiddenGradient"); opencl.KernelCreate(def_k_UpdateWeightsMomentum,"UpdateWeightsMomentum"); opencl.KernelCreate(def_k_UpdateWeightsAdam,"UpdateWeightsAdam"); opencl.KernelCreate(def_k_AttentionGradients,"AttentionInsideGradients"); opencl.KernelCreate(def_k_AttentionOut,"AttentionOut"); opencl.KernelCreate(def_k_AttentionScore,"AttentionScore"); opencl.KernelCreate(def_k_CalcHiddenGradientConv,"CalcHiddenGradientConv"); opencl.KernelCreate(def_k_CalcInputGradientProof,"CalcInputGradientProof"); opencl.KernelCreate(def_k_FeedForwardConv,"FeedForwardConv"); opencl.KernelCreate(def_k_FeedForwardProof,"FeedForwardProof"); opencl.KernelCreate(def_k_MatrixSum,"SumMatrix"); opencl.KernelCreate(def_k_Matrix5Sum,"Sum5Matrix"); opencl.KernelCreate(def_k_UpdateWeightsConvAdam,"UpdateWeightsConvAdam"); opencl.KernelCreate(def_k_UpdateWeightsConvMomentum,"UpdateWeightsConvMomentum"); opencl.KernelCreate(def_k_Normilize,"Normalize"); opencl.KernelCreate(def_k_NormilizeWeights,"NormalizeWeights"); opencl.KernelCreate(def_k_ConcatenateMatrix,"ConcatenateBuffers"); opencl.KernelCreate(def_k_DeconcatenateMatrix,"DeconcatenateBuffers"); opencl.KernelCreate(def_k_MHAttentionGradients,"MHAttentionInsideGradients"); opencl.KernelCreate(def_k_MHAttentionScore,"MHAttentionScore"); opencl.KernelCreate(def_k_MHAttentionOut,"MHAttentionOut"); opencl.KernelCreate(def_k_Dropout,"Dropout");
Аналогичное объявление нового кернела необходимо добавить в метод считывания предварительно обученной нейронной сети с диска CNet::Load.
Продолжая разговор о процессе загрузке предварительно обученной нейронной сети, следует скорректировать и метод создания элемента слоя нейронной сети CLayer::CreateElement, добавив туда код по созданию элемента Dropout. Изменения выделены заливкой.
bool CLayer::CreateElement(int index) < if(index>=m_data_max) return false; //--- bool result=false; CNeuronBase *temp=NULL; CNeuronProof *temp_p=NULL; CNeuronBaseOCL *temp_ocl=NULL; CNeuronConvOCL *temp_con_ocl=NULL; CNeuronAttentionOCL *temp_at_ocl=NULL; CNeuronMLMHAttentionOCL *temp_mlat_ocl=NULL; CNeuronDropoutOCL *temp_drop_ocl=NULL; if(iFileHandle0) < temp=new CNeuron(); if(CheckPointer(temp)==POINTER_INVALID || !temp.Init(iOutputs,index,SGD)) return false; result=true; > else < int type=FileReadInteger(iFileHandle); switch(type) < case defNeuron: temp=new CNeuron(); if(CheckPointer(temp)==POINTER_INVALID) result=false; result=temp.Init(iOutputs,index,ADAM); break; case defNeuronProof: temp_p=new CNeuronProof(); if(CheckPointer(temp_p)==POINTER_INVALID) result=false; if(temp_p.Init(iOutputs,index,1,1,1,ADAM)) < temp=temp_p; result=true; > break; case defNeuronConv: temp_p=new CNeuronConv(); if(CheckPointer(temp_p)==POINTER_INVALID) result=false; if(temp_p.Init(iOutputs,index,1,1,1,ADAM)) < temp=temp_p; result=true; > break; case defNeuronLSTM: temp_p=new CNeuronLSTM(); if(CheckPointer(temp_p)==POINTER_INVALID) result=false; if(temp_p.Init(iOutputs,index,1,1,1,ADAM)) < temp=temp_p; result=true; > break; case defNeuronBaseOCL: if(CheckPointer(OpenCL)==POINTER_INVALID) return false; temp_ocl=new CNeuronBaseOCL(); if(CheckPointer(temp_ocl)==POINTER_INVALID) result=false; if(temp_ocl.Init(iOutputs,index,OpenCL,1,ADAM)) < m_data[index]=temp_ocl; return true; > break; case defNeuronConvOCL: if(CheckPointer(OpenCL)==POINTER_INVALID) return false; temp_con_ocl=new CNeuronConvOCL(); if(CheckPointer(temp_con_ocl)==POINTER_INVALID) result=false; if(temp_con_ocl.Init(iOutputs,index,OpenCL,1,1,1,1,ADAM)) < m_data[index]=temp_con_ocl; return true; > break; case defNeuronAttentionOCL: if(CheckPointer(OpenCL)==POINTER_INVALID) return false; temp_at_ocl=new CNeuronAttentionOCL(); if(CheckPointer(temp_at_ocl)==POINTER_INVALID) result=false; if(temp_at_ocl.Init(iOutputs,index,OpenCL,1,1,ADAM)) < m_data[index]=temp_at_ocl; return true; > break; case defNeuronMHAttentionOCL: if(CheckPointer(OpenCL)==POINTER_INVALID) return false; temp_at_ocl=new CNeuronMHAttentionOCL(); if(CheckPointer(temp_at_ocl)==POINTER_INVALID) result=false; if(temp_at_ocl.Init(iOutputs,index,OpenCL,1,1,ADAM)) < m_data[index]=temp_at_ocl; return true; > break; case defNeuronMLMHAttentionOCL: if(CheckPointer(OpenCL)==POINTER_INVALID) return false; temp_mlat_ocl=new CNeuronMLMHAttentionOCL(); if(CheckPointer(temp_mlat_ocl)==POINTER_INVALID) result=false; if(temp_mlat_ocl.Init(iOutputs,index,OpenCL,1,1,1,1,0,ADAM)) < m_data[index]=temp_mlat_ocl; return true; > break; case defNeuronDropoutOCL: if(CheckPointer(OpenCL)==POINTER_INVALID) return false; temp_drop_ocl=new CNeuronDropoutOCL(); if(CheckPointer(temp_drop_ocl)==POINTER_INVALID) result=false; if(temp_drop_ocl.Init(iOutputs,index,OpenCL,1,0.1,ADAM)) < m_data[index]=temp_drop_ocl; return true; > break; default: result=false; break; > > if(result) m_data[index]=temp; //--- return (result); >
И конечно, добавим новый класс в диспетчерские методы базового класса CNeuronBaseOCL.
Прямой проход CNeuronBaseOCL::FeedForward.
bool CNeuronBaseOCL::FeedForward(CObject *SourceObject) < if(CheckPointer(SourceObject)==POINTER_INVALID) return false; //--- CNeuronBaseOCL *temp=NULL; switch(SourceObject.Type()) < case defNeuronBaseOCL: case defNeuronConvOCL: case defNeuronAttentionOCL: case defNeuronMHAttentionOCL: case defNeuronMLMHAttentionOCL: case defNeuronDropoutOCL: temp=SourceObject; return feedForward(temp); break; > //--- return false; >
Метод распространения градиента ошибки CNeuronBaseOCL::calcHiddenGradients.
bool CNeuronBaseOCL::calcHiddenGradients(CObject *TargetObject) < if(CheckPointer(TargetObject)==POINTER_INVALID) return false; //--- CNeuronBaseOCL *temp=NULL; CNeuronAttentionOCL *at=NULL; CNeuronMLMHAttentionOCL *mlat=NULL; CNeuronConvOCL *conv=NULL; CNeuronDropoutOCL *dropout=NULL; switch(TargetObject.Type()) < case defNeuronBaseOCL: temp=TargetObject; return calcHiddenGradients(temp); break; case defNeuronConvOCL: conv=TargetObject; temp=GetPointer(this); return conv.calcInputGradients(temp); break; case defNeuronAttentionOCL: case defNeuronMHAttentionOCL: at=TargetObject; temp=GetPointer(this); return at.calcInputGradients(temp); break; case defNeuronMLMHAttentionOCL: mlat=TargetObject; temp=GetPointer(this); return mlat.calcInputGradients(temp); break; case defNeuronDropoutOCL: dropout=TargetObject; temp=GetPointer(this); return dropout.calcInputGradients(temp); break; > //--- return false; >
И, как бы это не звучало странным, метод обновления весовых коэффициентов CNeuronBaseOCL::UpdateInputWeights.
bool CNeuronBaseOCL::UpdateInputWeights(CObject *SourceObject) < if(CheckPointer(SourceObject)==POINTER_INVALID) return false; //--- CNeuronBaseOCL *temp=NULL; switch(SourceObject.Type()) < case defNeuronBaseOCL: case defNeuronConvOCL: case defNeuronAttentionOCL: case defNeuronMHAttentionOCL: case defNeuronMLMHAttentionOCL: case defNeuronDropoutOCL: temp=SourceObject; return updateInputWeights(temp); break; > //--- return false; >
Помните, как бы не казались указанные выше изменения точечными и незначительными, отсутствие хотя бы одного из них ведет к неправильной работе всей нейронной сети.
С полным кодом всех классов и их методов можно ознакомиться во вложении.
3.Тестирование
Соблюдая принципы преемственности и наследования, базой для тестирования метода послужил советник из статьи [11] в который было добавлено 4 слоя Dropout:
- после исходных данных,
- после слоя эмбединга,
- после блока внимания,
- после первого полносвязного слоя.
Структура нейронной сети описана в коде ниже.
//--- 0 CLayerDescription *desc=new CLayerDescription(); if(CheckPointer(desc)==POINTER_INVALID) return INIT_FAILED; desc.count=(int)HistoryBars*12; desc.type=defNeuronBaseOCL; desc.optimization=ADAM; desc.activation=TANH; if(!Topology.Add(desc)) return INIT_FAILED; //--- 1 desc=new CLayerDescription(); if(CheckPointer(desc)==POINTER_INVALID) return INIT_FAILED; desc.count=(int)HistoryBars*12; desc.type=defNeuronDropoutOCL; desc.probability=0.2; desc.optimization=ADAM; desc.activation=TANH; if(!Topology.Add(desc)) return INIT_FAILED; //--- 2 desc=new CLayerDescription(); if(CheckPointer(desc)==POINTER_INVALID) return INIT_FAILED; desc.count=(int)HistoryBars; desc.type=defNeuronConvOCL; desc.window=12; desc.step=12; desc.window_out=24; desc.optimization=ADAM; desc.activation=SIGMOID; if(!Topology.Add(desc)) return INIT_FAILED; //--- 3 desc=new CLayerDescription(); if(CheckPointer(desc)==POINTER_INVALID) return INIT_FAILED; desc.count=(int)HistoryBars; desc.type=defNeuronDropoutOCL; desc.probability=0.2; desc.optimization=ADAM; desc.activation=SIGMOID; if(!Topology.Add(desc)) return INIT_FAILED; //--- 4 desc=new CLayerDescription(); if(CheckPointer(desc)==POINTER_INVALID) return INIT_FAILED; desc.count=(int)HistoryBars; desc.type=defNeuronMLMHAttentionOCL; desc.window=24; desc.window_out=4; desc.step=8; //heads desc.layers=5; desc.optimization=ADAM; desc.activation=SIGMOID; if(!Topology.Add(desc)) return INIT_FAILED; //--- 5 desc=new CLayerDescription(); if(CheckPointer(desc)==POINTER_INVALID) return INIT_FAILED; desc.count=(int)HistoryBars; desc.type=defNeuronDropoutOCL; desc.probability=0.2; desc.optimization=ADAM; desc.activation=SIGMOID; if(!Topology.Add(desc)) return INIT_FAILED; //--- 6 desc=new CLayerDescription(); if(CheckPointer(desc)==POINTER_INVALID) return INIT_FAILED; desc.count=200; desc.type=defNeuron; desc.activation=TANH; desc.optimization=ADAM; if(!Topology.Add(desc)) return INIT_FAILED; //--- 7 desc=new CLayerDescription(); if(CheckPointer(desc)==POINTER_INVALID) return INIT_FAILED; desc.count=200; desc.type=defNeuronDropoutOCL; desc.probability=0.2; desc.optimization=ADAM; desc.activation=TANH; if(!Topology.Add(desc)) return INIT_FAILED; //--- 8 desc=new CLayerDescription(); if(CheckPointer(desc)==POINTER_INVALID) return INIT_FAILED; desc.count=200; desc.type=defNeuron; desc.activation=TANH; desc.optimization=ADAM; if(!Topology.Add(desc)) return INIT_FAILED; //--- 9 desc=new CLayerDescription(); if(CheckPointer(desc)==POINTER_INVALID) return INIT_FAILED; desc.count=3; desc.type=defNeuron; desc.activation=SIGMOID; desc.optimization=ADAM;
Тестирование советника проводилось на инструмент EURUSD, таймфрейм H1, на вход нейронной сети подаются исторические данные за 20 последних свечей. Тестирование всех архитектур на схожих датасетах позволяет минимизировать влияние внешних факторов и оценить работу различных архитектур в схожих условиях.
Сравнивая графики обучения нейронной сети с использованием Dropout и без, можно заметить, что первые 30 эпох линии ошибки нейронной сети были практически параллельны, при этом нейронная сеть без Dropout показывала немногим лучший результат. Но после 33 эпохи наблюдается снижение показателя у советника с использованием Dropout. И после 35 эпохи Dropout показывает лучший результат, намечается тенденция к снижению показателя. При этом советник без Dropout продолжает удерживать ошибку на прежнем уровне.
Гр афик пропуска паттернов также показывает преимущества советника с использованием технологии Dropout. И данный график более красноречив. При худшем старте советник с использованием Dropout сразу показывает тенденцию к снижению пропусков. В то время, как советник без Dropout постепенно наращивает пропуски.
Графики уровня попадания предсказанных паттернов обоих советников лежат довольно близко и, практически, переплетаются. После 44 эпох обучения преимущества советника с Dropout составляет только 0.5%.

Заключение
В данной статье мы начали рассматривать методы повышения сходимости нейронных сетей и познакомились с одним из таких методов Dropout. Добавив его в один из ранее созданных советников, мы на тестах увидели его эффективность. Конечно, использование данного метода может увеличить затраты на обучение нейронной сети, но они будут оплачены повышением эффективности конечного результата.
Предлагаю всем попробовать данный метод в своих разработках и оценить его эффективность.
Ссылки
- Нейросети — это просто
- Нейросети — это просто (Часть 2): обучение и тестирование сети
- Нейросети — это просто (Часть 3): сверточные сети
- Нейросети — это просто (Часть 4): рекуррентные сети
- Нейросети — это просто (Часть 5): многопоточные вычисления в OpenCL
- Нейросети — это просто (Часть 6): эксперименты с коэффициентом обучения нейронной сети
- Нейросети — это просто (Часть 7): Адаптивные методы оптимизации
- Нейросети — это просто (Часть 8): Механизмы внимания
- Нейросети — это просто (Часть 9): Документируем проделанную работу
- Нейросети — это просто (Часть 10): Multi-Head Attention (многоголовое внимание)
- Нейросети — это просто (Часть 11): Вариации на тему GPT
- Improving neural networks by preventing co-adaptation of feature detectors
- Статистические оценки
Программы, используемые в статье
| # | Имя | Тип | Описание |
|---|---|---|---|
| 1 | Fractal_OCL_AttentionMLMH.mq5 | Советник | Советник с нейронной сетью классификации (3 нейрона в выходном слое) с использованием архитектуры GPT, 5 слоев внимания |
| 2 | Fractal_OCL_AttentionMLMH_d .mq5 | Советник | Советник с нейронной сетью классификации (3 нейрона в выходном слое) с использованием архитектуры GPT, 5 слоев внимания + Dropout |
| 3 | NeuroNet.mqh | Библиотека класса | Библиотека классов для создания нейронной сети |
| 4 | NeuroNet.cl | Библиотека | Библиотека кода программы OpenCL |
| 5 | NN.chm | HTML-справка | Скомпилированный CHM-файл помощи по библиотеке. |
Прикрепленные файлы |
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Другие статьи автора
- Нейросети — это просто (Часть 61): Проблема оптимизма в офлайн обучении с подкреплением
- Нейросети — это просто (Часть 60): Онлайн Трансформер решений (Online Decision Transformer—ODT)
- Нейросети — это просто (Часть 59): Дихотомия контроля (Dichotomy of Control — DoC)
- Нейросети — это просто (Часть 58): Трансформер решений (Decision Transformer—DT)
- Нейросети — это просто (Часть 57): Стохастический маргинальный актер-критик (SMAC)
- Нейросети — это просто (Часть 56): Использование ядерной нормы для стимулирования исследования
- Нейросети — это просто (Часть 55): Контрастный внутренний контроль (CIC)