Нейронная сеть на практике: Функции активации
Введение
В предыдущей статье мы разработали приложение, с помощью которого могли визуализировать график функции активации вместе с её производной. Однако то, что мы там увидели, — лишь основа для чего-то немного более сложного, и именно это станет главной темой данной статьи.
Я знаю, что многие могут считать ненужным разбираться в графике функции активации и её производной и что мы могли бы сразу углубиться в тему, оставив такие вопросы в стороне. Однако гораздо проще понять, почему сигмоидную функцию нельзя использовать в очень глубоких сетях, если вы понимаете её график.
Новое приложение
Всё, что было сказано в предыдущей статье, по-прежнему остаётся в силе. Помимо того, что мы там увидели, здесь мы рассмотрим новый код. Также мы кратко объясним каждую из выбранных функций. Прежде чем начать, хочу кое-что вам напомнить, мой читатель. Существует огромное количество функций активации. Некоторые из них популярнее других. Тем не менее, у каждой из них есть своё назначение и конкретная область применения, чтобы тем или иным образом оптимизировать скорость обучения. Далее мы рассмотрим, как некоторые из этих функций используются на практике, чтобы вы могли убедиться, насколько сильно даже простое изменение выбора может повлиять на результаты, которые мы можем получить в процессе обучения сети. В любом случае, сейчас мы сосредоточимся только на действительно интересной части системы, где будем использовать функции полностью изолированно. Так будет проще понять, какой выбор может оказаться лучшим в тех или иных ситуациях. В любом случае, не беспокойтесь об этом сейчас. Хотя математическая часть довольно сложная и запутанная, в конце мы постараемся значительно её упростить. Я хочу, чтобы вы действительно смогли понять, как работает нейронная сеть. Цель не в том, чтобы сказать вам: «Сделайте это. Не делайте того». Подобные указания ничего не добавляют к вашим знаниям. Это лишь создаёт ложную иллюзию того, что вы понимаете то, чего на самом деле не понимаете.
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. #property indicator_chart_window 004. #property indicator_plots 0 005. //+------------------------------------------------------------------+ 006. #include <Canvas\Canvas.mqh> 007. //+------------------------------------------------------------------+ 008. enum eFnActivate { 009. Sigmoid, 010. Tangh, 011. ReLU, 012. eLU, 013. SoftSign, 014. SoftPlus, 015. }; 016. //+------------------------------------------------------------------+ 017. input eFnActivate user_00 = Sigmoid; //Функция активации 018. input double user_01 = 10.0; //Пределы 019. input double user_02 = 0.1; //Детализация 020. input double user_03 = 9.0; //Масштаб по оси Y 021. //+------------------------------------------------------------------+ 022. CCanvas *canvas; 023. //+------------------------------------------------------------------+ 024. struct st_00 025. { 026. int x, 027. y, 028. maxX, 029. maxY; 030. double Position, 031. dx, fx; 032. }global; 033. //+------------------------------------------------------------------+ 034. void PlotTextPosition(void) 035. { 036. uint w, h; 037. string sz0; 038. 039. sz0 = StringFormat("%s Position: [%.1f] << fx: %.8f dx: %.8f >>", EnumToString(user_00), global.Position, global.fx, global.dx); 040. TextGetSize(sz0, w, h); 041. (*canvas).TextOut(global.x - (w / 2), global.maxY + 5, sz0, ColorToARGB(clrBlack)); 042. } 043. //+------------------------------------------------------------------+ 044. void Function_Curve(void) 045. { 046. int x[], y[], d[]; 047. uint p; 048. double fx, dx, cx, step, alpha; 049. 050. ArrayResize(x, (int)(((user_01 * 2) / user_02) + 1)); 051. ArrayResize(y, x.Size()); 052. ArrayResize(d, x.Size()); 053. 054. step = MathMin(global.maxY, global.maxX) / (x.Size() * 1.0) / user_02; 055. 056. cx = -user_01; 057. for (uint c = 0, m = x.Size(); c < m; c++, cx += user_02) 058. { 059. x[c] = global.x + (int)(step * cx); 060. switch (user_00) 061. { 062. case Sigmoid: 063. fx = 1 / (1 + MathExp(-cx)); 064. dx = MathExp(-cx) / MathPow(1 + MathExp(-cx), 2); 065. break; 066. case Tangh: 067. fx = (2 / (1 + MathExp(-2 * cx))) - 1; 068. dx = (4 / MathPow(MathExp(-cx) + MathExp(cx), 2)); 069. break; 070. case ReLU: 071. fx = MathMax(0, cx); 072. dx = (cx <= 0 ? 0 : 1); 073. break; 074. case eLU: 075. alpha = 0.75; 076. fx = (cx <= 0 ? (alpha * (MathExp(cx) - 1)) : cx); 077. dx = (cx <= 0 ? (alpha * MathExp(cx)) : 1); 078. break; 079. case SoftSign: 080. fx = cx / (1 + MathAbs(cx)); 081. dx = cx / MathPow(1 + MathAbs(cx), 2); 082. break; 083. case SoftPlus: 084. fx = MathLog(1 + MathExp(cx)); 085. dx = 1 / (1 + MathExp(-cx)); 086. break; 087. default: 088. fx = dx = 0; 089. break; 090. } 091. if (cx <= global.Position) 092. { 093. p = c; 094. global.fx = fx; 095. global.dx = dx; 096. } 097. y[c] = global.y - (int)(step * fx * user_03); 098. d[c] = global.y - (int)(step * dx * user_03); 099. } 100. 101. (*canvas).PolylineThick(x, y, ColorToARGB(clrIndigo, 255), 3, STYLE_SOLID, LINE_END_ROUND); 102. (*canvas).PolylineThick(x, d, ColorToARGB(clrDarkOrange, 255), 3, STYLE_SOLID, LINE_END_ROUND); 103. 104. (*canvas).FillCircle(x[p], y[p], 5, ColorToARGB(clrForestGreen, 255)); 105. (*canvas).FillCircle(x[p], d[p], 5, ColorToARGB(clrFireBrick, 255)); 106. 107. ArrayFree(d); 108. ArrayFree(y); 109. ArrayFree(x); 110. } 111. //+------------------------------------------------------------------+ 112. void UpdateGraphics(int direct) 113. { 114. double value = global.Position + user_02 * direct; 115. 116. global.Position = MathAbs(value) <= user_01 ? value : global.Position; 117. 118. (*canvas).Erase(ColorToARGB(clrWhite, 255)); 119. 120. (*canvas).LineVertical(global.x, 0, global.maxY, ColorToARGB(clrRoyalBlue, 255)); 121. (*canvas).LineHorizontal(0, global.maxX, global.y, ColorToARGB(clrRoyalBlue, 255)); 122. 123. Function_Curve(); 124. PlotTextPosition(); 125. 126. (*canvas).Update(true); 127. } 128. //+------------------------------------------------------------------+ 129. void Resize(void) 130. { 131. uint w, h; 132. 133. global.maxX = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0); 134. global.maxY = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, 0); 135. 136. if (canvas != NULL) 137. { 138. (*canvas).Destroy(); 139. delete canvas; 140. } 141. canvas = new CCanvas; 142. (*canvas).CreateBitmapLabel("BL", 0, 0, global.maxX, global.maxY, COLOR_FORMAT_ARGB_NORMALIZE); 143. global.x = global.maxX / 2; 144. global.y = global.maxY / 2; 145. TextGetSize("M", w, h); 146. global.maxY -= (int)(h * 2); 147. } 148. //+------------------------------------------------------------------+ 149. int OnInit() 150. { 151. ChartSetInteger(0, CHART_SHOW_DATE_SCALE, false); 152. ChartSetInteger(0, CHART_SHOW_PRICE_SCALE, false); 153. ZeroMemory(global); 154. canvas = NULL; 155. 156. return INIT_SUCCEEDED; 157. } 158. //+------------------------------------------------------------------+ 159. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 160. { 161. return rates_total; 162. } 163. //+------------------------------------------------------------------+ 164. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 165. { 166. int direct = 0; 167. 168. switch (id) 169. { 170. case CHARTEVENT_CHART_CHANGE: 171. Resize(); 172. break; 173. case CHARTEVENT_KEYDOWN: 174. if (TerminalInfoInteger(TERMINAL_KEYSTATE_LEFT)) direct = -1; 175. if (TerminalInfoInteger(TERMINAL_KEYSTATE_RIGHT)) direct = 1; 176. break; 177. } 178. UpdateGraphics(direct); 179. } 180. //+------------------------------------------------------------------+ 181. void OnDeinit(const int reason) 182. { 183. (*canvas).Destroy(); 184. delete canvas; 185. ChartSetInteger(0, CHART_SHOW_DATE_SCALE, true); 186. ChartSetInteger(0, CHART_SHOW_PRICE_SCALE, true); 187. } 188. //+------------------------------------------------------------------+
Да, этот код действительно выглядит заметно сложнее. Но не волнуйтесь. Я объясню каждую часть, чтобы вы могли это понять. На самом деле это проще, чем предыдущий код. Во-первых, в строке 8 было добавлено перечисление. Его цель — дать нам возможность выбрать нужное уравнение. Каждое из названий, включённых в это перечисление, на самом деле соответствует названию уравнения.
Эти шесть уравнений используются в функциях активации. Как я уже упоминал, есть и другие. Многие из них представляют собой небольшие вариации тех, что приведены в этом списке. Если хотите, можете без проблем добавить их. Нужно соблюдать лишь одну предосторожность: поместить математическое выражение в нужное место и в правильной форме. Если вы это сделаете, то сможете построить график любой математической функции, которую захотите. И всё это довольно просто и без особых сложностей.
Итак, теперь, когда у нас уже есть названия уравнений, следующим изменением стал способ, которым пользователь выбирает уравнение для графического отображения. Раньше нам приходилось компилировать код каждый раз, когда мы вносили изменения — будь то в уравнение или в используемые параметры. Однако, как я уже упоминал в начале, цель здесь — дать пользователю больше интерактивности. Таким образом, вам не придётся постоянно компилировать код. Для этого были предусмотрены четыре входных параметра. Раньше у нас было два: диапазон значений и шаг построения кривой. Теперь к ним добавлены ещё два: выбор уравнения и коэффициент масштабирования по оси Y. Это связано с тем, что в некоторых случаях значения по оси Y оказываются очень близки к 1,0. В таком случае стоит предоставить пользователю возможность изменять масштаб оси Y. Таким образом, масштаб графика изменится, и построенную кривую будет проще анализировать.

Обратите внимание, что в отображении были изменены некоторые вещи. На самом деле изменение коснулось только той части, которая относится к отображаемому тексту. Раньше мы отображали только положение по оси X. Теперь, помимо этого значения, мы также отображаем название уравнения, его значение по оси Y в зависимости от значения по оси X, а также значение производной в той же точке по оси X. И всё это удалось сделать, изменив всего несколько строк кода. По сути, значения fx и dx, которые вы видите на изображении, отображаются там благодаря строке 39 кода. Обратите внимание, что в этой строке мы обращаемся к перечислению, чтобы получить имя функции. Мы также получаем доступ к значениям, заданным в строке 31. Эти две переменные в строке 31 раньше не существовали.
Итак, поскольку большая часть кода осталась без изменений, единственными изменениями стали добавленные строки 151, 152, 185 и 186, цель которых — сделать окно графика как можно чище. Поскольку это легко понять, мы можем сосредоточить внимание на главном: на процедуре в строке 44.
043. //+------------------------------------------------------------------+ 044. void Function_Curve(void) 045. { 046. int x[], y[], d[]; 047. uint p; 048. double fx, dx, cx, step, alpha; 049. 050. ArrayResize(x, (int)(((user_01 * 2) / user_02) + 1)); 051. ArrayResize(y, x.Size()); 052. ArrayResize(d, x.Size()); 053. 054. step = MathMin(global.maxY, global.maxX) / (x.Size() * 1.0) / user_02; 055. 056. cx = -user_01; 057. for (uint c = 0, m = x.Size(); c < m; c++, cx += user_02) 058. { 059. x[c] = global.x + (int)(step * cx); 060. switch (user_00) 061. { 062. case Sigmoid: 063. fx = 1 / (1 + MathExp(-cx)); 064. dx = MathExp(-cx) / MathPow(1 + MathExp(-cx), 2); 065. break; 066. case Tangh: 067. fx = (2 / (1 + MathExp(-2 * cx))) - 1; 068. dx = (4 / MathPow(MathExp(-cx) + MathExp(cx), 2)); 069. break; 070. case ReLU: 071. fx = MathMax(0, cx); 072. dx = (cx <= 0 ? 0 : 1); 073. break; 074. case eLU: 075. alpha = 0.75; 076. fx = (cx <= 0 ? (alpha * (MathExp(cx) - 1)) : cx); 077. dx = (cx <= 0 ? (alpha * MathExp(cx)) : 1); 078. break; 079. case SoftSign: 080. fx = cx / (1 + MathAbs(cx)); 081. dx = cx / MathPow(1 + MathAbs(cx), 2); 082. break; 083. case SoftPlus: 084. fx = MathLog(1 + MathExp(cx)); 085. dx = 1 / (1 + MathExp(-cx)); 086. break; 087. default: 088. fx = dx = 0; 089. break; 090. } 091. if (cx <= global.Position) 092. { 093. p = c; 094. global.fx = fx; 095. global.dx = dx; 096. } 097. y[c] = global.y - (int)(step * fx * user_03); 098. d[c] = global.y - (int)(step * dx * user_03); 099. } 100. 101. (*canvas).PolylineThick(x, y, ColorToARGB(clrIndigo, 255), 3, STYLE_SOLID, LINE_END_ROUND); 102. (*canvas).PolylineThick(x, d, ColorToARGB(clrDarkOrange, 255), 3, STYLE_SOLID, LINE_END_ROUND); 103. 104. (*canvas).FillCircle(x[p], y[p], 5, ColorToARGB(clrForestGreen, 255)); 105. (*canvas).FillCircle(x[p], d[p], 5, ColorToARGB(clrFireBrick, 255)); 106. 107. ArrayFree(d); 108. ArrayFree(y); 109. ArrayFree(x); 110. } 111. //+------------------------------------------------------------------+
Если вы сравните этот код с тем, что мы рассматривали в предыдущей статье, то сразу заметите, что здесь появились две новые переменные. Они находятся в строке 48. Даже переменные fx и dx изменили своё первоначальное назначение. Теперь обе переменные будут использоваться для временного хранения вычисленных значений.
Это может показаться сложным, но, если вы раньше никогда не программировали, будьте внимательны, потому что сейчас вы научитесь делать это всего за несколько шагов.
Предположим, вы хотите добавить в это приложение новую функцию активации, чтобы можно было графически отобразить как результат уравнения, так и его производную. Как вам следует поступить? Итак, для начала перейдите к строке 8, где указаны названия функций, и добавьте название своей новой функции. Помните, что в имени НЕЛЬЗЯ использовать пробелы. Если нужно разделить слова, используйте символ подчёркивания ( _ ).
После этого вы сможете выбрать это уравнение при использовании скомпилированного приложения в MetaTrader 5. Однако пока вы не увидите никакого графика. Чтобы построить график, вам нужно просто перейти к процедуре, показанной в предыдущем фрагменте, и сделать следующее: в строке 60 у нас есть оператор switch. Он будет выбирать, какой код нужно выполнить, в зависимости от значения, соответствующего уравнению, выбранному пользователем.
Таким образом, в строке 61 после открывающей фигурной скобки нужно нажать ENTER, ввести «case», поставить пробел и сразу после этого ввести название вашего уравнения, за которым следует двоеточие ( : ). Снова нажмите ENTER и введите «break», а затем точку с запятой ( ; ). Таким образом вы создадите структуру, очень похожую на ту, что приведена в коде. А теперь самое интересное. Между двоеточием ( : ) и словом «break» нужно написать нечто очень похожее на то, что приведено в каждой из структур между строками 62 и 86. Обратите внимание, что у нас есть fx = одно значение и dx = другое. И что это значит? Итак, в fx нужно указать выражение функции, а в dx — выражение её производной. И это всё, что вам нужно сделать. Остальное делает сам код или это выполняется во время его использования в MetaTrader 5. Если вы не поняли, как работать с оператором switch, ознакомьтесь с другой моей статьёй. В этой статье я подробно объясняю, как работать с оператором switch.
Сигмоидальная функция активации
Эта функция уже рассматривалась в другой статье, где я показал, что для выхода из состояния застоя необходимо использовать функцию активации. Ознакомьтесь с предыдущими статьями, чтобы лучше понять эту тему. Итак, когда мы добавим индикатор на график и выберем сигмоиду, сможем взаимодействовать с графиком, как показано в следующей анимации.



Однако, если просто посмотреть на эти математические выражения, вы, возможно, не сразу заметите, насколько сильно производная сигмоиды уменьшает значения. В некоторой степени это довольно неудобно, поскольку имеет важные последствия, когда мы хотим использовать эту функцию в более глубокой сети. Нужно быть осторожными, решая, в какой точке сети мы будем использовать сигмоиду. Если использовать её без разбора, в какой-то момент вы помешаете сети продолжать распространение данных. Это станет понятнее позже. Тот, кто немного разбирается в математике и производных, вероятно, уже думает о следующей теме: ПРАВИЛО ЦЕПИ. Так называется эта проблема. Однако реально это влияет на нас только при использовании производных и только во время обратного распространения. Как я уже упоминал, мы рассмотрим всё это более подробно позже.
Даже зная об этой проблеме, сигмоида — чрезвычайно полезная функция, потому что она нормализует выходные значения перцептрона, удерживая их в диапазоне от нуля до единицы. Кроме того, её часто используют, когда нужна модель, позволяющая предсказывать вероятность чего-либо по выходному значению нейронной сети. Поскольку выходное значение всегда находится в диапазоне от нуля до единицы, оно идеально подходит для преобразования данных в проценты. У сигмоиды есть и ещё одно большое преимущество, если использовать её осторожно: она позволяет избежать резких изменений во время градиентного спуска. Это связано с тем, что эта функция дифференцируема, а значит, мы можем определить наклон кривой, используя любые две точки.
Функция активации «гиперболический тангенс»
Эта функция активации пытается решить одну из проблем сигмоиды: она не принимает отрицательных значений. Кроме того, в некоторых случаях тот факт, что сигмоида ограничена значениями от нуля до единицы, создаёт дополнительное неудобство. Это заметно, когда её значения приближаются к нулю. Во многих случаях этот эффект может оказаться разрушительным для сети перцептронов, поскольку он практически «убивает» некоторые из используемых весов, из-за чего они в итоге полностью игнорируются, хотя на самом деле могли бы помочь классифицировать новые данные, поступающие в сеть.
Итак, при выборе функции «гиперболический тангенс» в приложении вы увидите следующую анимацию.

Что касается уравнения, в его записи есть небольшие вариации. Одна из них приведена в коде, в строке шестьдесят семь, и именно она показана ниже.

Есть и другой способ записать это же уравнение. Его можно увидеть ниже.

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

Несмотря на эти различия в записи уравнения, часть, относящаяся к его производной, меняется не так сильно. Тем не менее, у нас есть несколько способов её выразить. В коде вы можете увидеть производную в строке шестьдесят восемь, где используется уравнение, показанное ниже.

Мы также можем использовать уравнение, подобное приведённому ниже, которое тоже позволяет получить производную.


Конечно, все эти выражения — лишь небольшая часть разных способов представить одно и то же. То есть не стоит слепо придерживаться формул или выражений, которые вы где-то найдёте, только потому, что кто-то сказал, что это лучший вариант. Как я уже упоминал, человечество пока не знает, как именно построить универсальную сеть перцептронов. Единственное, что нам удалось понять, — это как создать сеть, которая более или менее адекватно адаптируется к определённому типу задач.
Хорошо, продолжим, потому что в этой статье ещё осталось кое-что показать.
Функция активации ReLU
Эту функцию мы уже рассматривали в предыдущей статье. Поэтому здесь мы быстро по ней пройдёмся. При её выборе появится следующий график, с которым мы сможем взаимодействовать.



Функция активации eLU
Это одна из множества существующих вариаций функции ReLU. Об этом упоминалось в предыдущем разделе именно для того, чтобы показать её математическое выражение, чтобы, если вы никогда не слышали о вариациях математических выражений, вы могли, так сказать, сравнить одну из них с исходным выражением.



"Боже мой, для этих двух выражений точно придётся вызывать экзорциста. Какая жуть". Что ж, независимо от того, что вы сейчас думаете и насколько сильно вас могут пугать эти выражения, в обоих можно заметить нечто общее: греческую букву АЛЬФА. Она находится в строке 75 кода. Назначение этого параметра альфа — изменять кривизну уравнения. В зависимости от того, какое значение вы ему зададите, можно получить более крутую или более пологую кривую. Или, другими словами, радиус дуги, которую вы видите на графике, может стать немного меньше или больше. Это создаёт небольшую разницу в выходных значениях перцептрона. Однако, если посмотреть на оба выражения, можно заметить, что, в отличие от исходной функции ReLU, у этой функции eLU нет проблем с нулём, поскольку само уравнение может обрабатывать нулевое значение в производной.
Функция активации SoftSign



Если вы внимательно присмотритесь, то заметите, что SoftSign находится где-то между сигмоидой и гиперболическим тангенсом. Однако в определённых ситуациях она может оказаться гораздо более подходящей, чем любая из них. В зависимости от задачи, которую вы хотите решить, она может помочь градиенту находить минимум несколько эффективнее, чем сигмоида или гиперболический тангенс. Кроме того, благодаря способу вычисления эта функция может вычисляться быстрее в более глубоких сетях. И это в некоторых случаях даёт заметную разницу во время обучения. Модели, использующие TensorFlow, часто отдают предпочтение этой функции вместо сигмоиды и гиперболического тангенса именно из-за этого фактора во время обучения модели.
Функция активации SoftPlus

Вы можете ясно увидеть, что она довольно сильно сглаживает переход между отрицательными и положительными значениями, следуя той же идее, что и исходная ReLU. Однако, в отличие от исходной ReLU и eLU, которые мы только что рассмотрели, SoftPlus ведёт себя гораздо мягче и плавнее. То есть, начиная с определённого значения, переход в окрестности нуля становится гораздо более плавным. Это отличается от ReLU, которая просто резко переключается к нулю или единице в зависимости от того, отрицательны значения или положительны.


Заключительные замечания
Признаюсь, конечный результат в этой статье меня в итоге удивил, поскольку он оказался гораздо более интерактивным. Вам не нужно вносить большие изменения в код, чтобы добавить новые функции, с помощью которых можно изучать возможные математические модели. Всё, что вам нужно будет сделать, — это добавить название уравнения, а затем его математическое выражение.
Таким образом, в этой статье вы не только увидели, как разные функции активации по-разному влияют на выход перцептрона, но и узнали, что небольшие изменения в способе выполнения вычислений могут сильно повлиять на выбор функции активации. Помните, что не все перцептронные сети используют такие функции, как показанные здесь. Однако об этом мы поговорим в другой раз.
Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/13909
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
От базового к среднему уровню: Ресурсы
Нейросети в трейдинге: Диффузионная генерация торговых планов (DPCC)
От начального к среднему уровню: Объекты и подокна (I)
От начального к среднему уровню: Подокна (IV)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования