Как можно упростить и ускорить вычисление нейронной сети прямого распространения

Здравствуйте, уважаемые читатели. О нейронных сетях написано и сказано очень много, преимущественно о том, как и для чего их можно применить. При этом как-то не очень много внимания уделяется двум важным вопросам: а) как нейронную сеть упростить и быстро вычислить (одно вычисление экспоненты реализуется библиотечными функциями языков программирования, обычно, не менее чем за 15–20 процессорных инструкций), б) какова, хотя бы отчасти, логика работы построенной сети — в самом деле, получаемые после обучении сети огромные матрицы значений весов и смещений как-то не очень помогают понять закономерности, которые эта сеть нашла (они остаются скрытыми и задача их определить — задача вербализации — иногда очень важна). Я расскажу об одном своем подходе к решению этих вопросов для обычных нейронных сетей прямого распространения, при этом постараюсь обойтись минимумом математики.

Немного теории


Сеть прямого распространения, с математической точки зрения — очень большая функция, в которую входят значения входов сети, весовых коэффициентов и смещений нейронов. В каждом нейроне слоя значения входов слоя (вектор X) умножаются на веса нейрона (вектор $W_i$), складываются со смещением $B_i$

$s_i = W_iX+B_i$


и поступают в активационные функции $A(s_i)$, формирующие выходы нейронов слоя.

Активационные функции могут быть не очень простыми для вычисления, например, часто содержат экспоненты (экспоненциальная сигмоида, гиперболический тангенс). Если заглянуть в ассемблерный код, реализующий экспоненты, то можно обнаружить, во-первых, множество различных проверок, которые не всегда нужны, во-вторых, само вычисление экспоненты обычно производится минимум за две операции:

$exp(v)=2^{v*log_2(e)}$

.
Поэтому, если мы хотим ускорить расчет сети, то первой задачей будет упрощение расчета активационной функции. Можно попробовать немного пожертвовать качеством за счет выигрыша в скорости, приближенно заменив расчет классической активационной функции на расчет более простой функции, которая (на имеющихся входных данных) дает примерно те же результаты. Это, вообще говоря, классическая задача интерполяции: имеем набор значений, вычисленных исходной функцией A (s), и подбираем более простую функцию, которая дает очень похожие значения. Такой простой функцией a (s) может быть обычный полином, или полином с отрицательными степенями или еще что-то в таком роде. Я использовал четыре вида таких функций:

$a(s) = b_0 + b_1*s + b_2*s^2 + … + b_n*s^n$;
$a(s) = b_0 + b_1/s + b_2/s^2 + … + b_n/s^n$;
$a(s) = b_0 + b_1*s^{0,5} + b_2*s^1 + b_3*s^{1,5} + … + b_n*s^{0,5n}$;
$a(s) = b_0 + b_1/s^{0,5} + b_2/s^1 + b_3/s^{1,5} + … + b_n/s^{0,5n}$;

Предположим, что для каждого нейрончика нам удалось заменить активационную функцию на немного более простую — это можно сделать, например, применив метод наименьших квадратов. Сама по себе такая замена очень большого выигрыша, возможно, и не даст. Но тут можно попробовать еще один прием:

  1. Записать аналитически огромную функцию NET (X), вычисляемую сетью в-целом;
  2. Заменить в NET (X) исходные функции A (s) на полученные для них заменяющие функции a (s);
  3. Алгебраически полученную NET (X) упростить (вернее, воспользоваться каким-либо готовым программным кодом символического упрощения выражений). Это уже возможно (по крайней мере, намного проще, чем мы бы пытались упростить сеть с исходными функциями, например, с экспонентами).


В результате получаем нечто более простое и, возможно, немного более математически наглядное — здесь уже можно пытаться понять, какой вид имеет реализуемая сетью функция.

Это и есть вариант объяснения логики работы построенной сети.

Описанная задача, конечно, лишь на словах выглядит простой. Для применения в своих программах мне потребовалось написать собственный код символического упрощения выражений. Кроме того, я решал более сложную задачу, допустив, что у каждого нейрона с функцией A (s) может быть несколько вариантов альтернативной активационной функции $a_k(s)$, поэтому общая задача свелась еще и к перебору вариантов таких функций и символическому упрощению сети для каждого такого варианта. Тут уже помогло только распараллеливание вычислений.

Результат


Результат меня порадовал. Я ускорял трехслойную сеть (с тремя входами) из восьми нейронов (с входными весами и смещениями) с активационными функциями «экспоненциальная сигмоида». Как показали замеры времени, удалось получить выигрыш примерно в 40% по времени без существенных потерь в качестве.

Иллюстрирую. Вот данные исходной сети:

fs9bfh-pby7ugyd_vwnkc9mlvt8.png

kyj-wu-hmbtreqoi8ngqxniedzm.png

И в третьем, выходном слое:
4gucud_k7puheqscdocjswtdtnu.png

Если обозначить входы как a, b и c, то, после замен и упрощений, сетевая функция NET считается так:

double a2 = a*a;
double b2 = b*b;
double c2 = c*c;
double a3 = a2*a;
double b3 = b2*b;
double c3 = c2*c;
double z01 = sqrt(-1.6302e-02+7.9324e-01*a+9.65149e-01*b+5.64151e-01*c);
double z06 = sqrt(1.583708e+00-8.907654e-01*a-2.844379e-01*a2+1.050942e+00*a3+1.178096e+01*b-1.865618e+00*b*a-3.145465e+00*b*a2-5.777153e+00*b2+3.138123e+00*b2*a-1.043599e+00*b3+1.32778e+00*c+5.849582e-01*c*a-3.440382e+00*c*a2+1.838371e+00*c*b+6.864703e+00*c*b*a-3.42434e+00*c*b2-3.013361e-01*c2+3.754167e+00*c2*a-3.745404e+00*c2*b-1.365524e+00*c3+1.014237e-01*z01);
double NET = (-1.477593e+00)/(z06)+1.370237e+00-6.303167e-02*a-1.495051e-03*a2+2.33748e-02*a3+5.558024e-02*b+1.178189e-02*b*a-6.996071e-02*b*a2+1.837937e-02*b2+6.97974e-02*b2*a-2.321149e-02*b3+7.924241e-02*c+3.392287e-03*c*a-7.652018e-02*c*a2-1.214263e-02*c*b+1.526831e-01*c*b*a-7.616337e-02*c*b2-1.915279e-03*c2+8.349931e-02*c2*a-8.33044e-02*c2*b-3.037166e-02*c3+1.949161e-02*z01;

Выигрыш — повторюсь, 40% по времени, без особого ущерба качеству. Думаю, можно применить такой подход в случаях, когда скорость вычисления нейронной сети критична — например, если она вычисляется многократно, в двойном или тройном цикле. Пример такой задачи: численное решение задачи аэродинамики на сетке, причем в каждом ее узле нейронная сеть вычисляет какой-либо полезный прогноз, например, для более точного расчета турбулентной вязкости. Тогда имеем внешний цикл по времени, в него вложен двойной или тройной цикл по координате и уже там, внутри, сидит расчет нейронной сети. В таком случае упрощение более чем уместно и полезно.

© Habrahabr.ru