Сигмоидная функция определяется как
Я обнаружил, что использование встроенной функции C exp()
для вычисления значения f(x)
является медленным. Есть ли более быстрый алгоритм для вычисления значения f(x)
?
Сигмоидная функция определяется как
Я обнаружил, что использование встроенной функции C exp()
для вычисления значения f(x)
является медленным. Есть ли более быстрый алгоритм для вычисления значения f(x)
?
вам не нужно использовать фактическую точную сигмовидную функцию в алгоритме нейронной сети, но она может заменить ее аппроксимированной версией, которая имеет схожие свойства, но быстрее вычисляет.
Например, вы можете использовать функцию "быстрый сигмоид"
f(x) = x / (1 + abs(x))
Использование первых членов разложения в ряд для exp (x) не поможет слишком много, если аргументы f (x) не близки к нулю, и у вас есть та же проблема с последовательным расширением сигмоидальной функции, если аргументы "большие".
Альтернативой является использование поиска в таблице. То есть вы предварительно вычисляете значения сигмоидной функции для заданного количества точек данных, а затем выполняете быструю (линейную) интерполяцию между ними, если хотите.
Лучше всего измерить на вашем оборудовании. Просто быстрый тест script показывает, что на моей машине 1/(1+|x|)
самый быстрый, а tanh(x)
- второй. Функция ошибки erf
также довольно быстро.
% gcc -Wall -O2 -lm -o sigmoid-bench{,.c} -std=c99 && ./sigmoid-bench
atan(pi*x/2)*2/pi 24.1 ns
atan(x) 23.0 ns
1/(1+exp(-x)) 20.4 ns
1/sqrt(1+x^2) 13.4 ns
erf(sqrt(pi)*x/2) 6.7 ns
tanh(x) 5.5 ns
x/(1+|x|) 5.5 ns
Я ожидаю, что результаты могут отличаться в зависимости от архитектуры и используемого компилятора, но erf(x)
(начиная с C99), tanh(x)
и x/(1.0+fabs(x))
, скорее всего, будут быстрыми исполнителями.
В основном люди обеспокоены тем, насколько быстро одна функция относится к другой, и создайте микро-тест, чтобы увидеть, работает ли f1(x)
на 0,0001 мс быстрее, чем f2(x)
. Большая проблема заключается в том, что это в основном не имеет значения, потому что важно то, как быстро ваша сеть учится с вашей функцией активации, пытаясь минимизировать вашу функцию затрат.
В соответствии с текущей теорией выпрямительная функция и softplus
по сравнению с сигмовидной функцией или аналогичными функциями активации, разрешите для более быстрого и эффективного обучения глубоких нейронных архитектур на больших и сложных наборов данных.
Итак, я предлагаю отбросить микро-оптимизацию и взглянуть на то, какая функция позволяет быстрее учиться (а также смотреть на другую функцию стоимости).
Чтобы сделать NN более гибким, обычно используется некоторая альфа-скорость, чтобы изменить угол графика вокруг 0.
Сигмоидная функция выглядит так:
f(x) = 1 / ( 1+exp(-x*alpha))
Почти эквивалентная (но более быстрая функция):
f(x) = 0.5 * (x * alpha / (1 + abs(x*alpha)) + 0.5
Вы можете проверить графики здесь
Когда я использую функцию abs, сеть становится быстрее 100 раз.
Этот ответ, вероятно, не имеет отношения к большинству случаев, но просто хотел выбросить туда, что для вычислений CUDA я нашел x/sqrt(1+x^2)
как бы самую быструю функцию.
Например, выполняется с помощью встроенных операций с плавающей запятой:
__device__ void fooCudaKernel(/* some arguments */) {
float foo, sigmoid;
// some code defining foo
sigmoid = __fmul_rz(rsqrtf(__fmaf_rz(foo,foo,1)),foo);
}
Также вы можете использовать грубую версию сигмоида (она отличается не более чем на 0,2% от оригинала):
inline float RoughSigmoid(float value)
{
float x = ::abs(value);
float x2 = x*x;
float e = 1.0f + x + x2*0.555f + x2*x2*0.143f;
return 1.0f / (1.0f + (value > 0 ? 1.0f / e : e));
}
void RoughSigmoid(const float * src, size_t size, const float * slope, float * dst)
{
float s = slope[0];
for (size_t i = 0; i < size; ++i)
dst[i] = RoughSigmoid(src[i] * s);
}
Оптимизация функции RoughSigmoid с использованием SSE:
#include <xmmintrin.h>
void RoughSigmoid(const float * src, size_t size, const float * slope, float * dst)
{
size_t alignedSize = size/4*4;
__m128 _slope = _mm_set1_ps(*slope);
__m128 _0 = _mm_set1_ps(-0.0f);
__m128 _1 = _mm_set1_ps(1.0f);
__m128 _0555 = _mm_set1_ps(0.555f);
__m128 _0143 = _mm_set1_ps(0.143f);
size_t i = 0;
for (; i < alignedSize; i += 4)
{
__m128 _src = _mm_loadu_ps(src + i);
__m128 x = _mm_andnot_ps(_0, _mm_mul_ps(_src, _slope));
__m128 x2 = _mm_mul_ps(x, x);
__m128 x4 = _mm_mul_ps(x2, x2);
__m128 series = _mm_add_ps(_mm_add_ps(_1, x), _mm_add_ps(_mm_mul_ps(x2, _0555), _mm_mul_ps(x4, _0143)));
__m128 mask = _mm_cmpgt_ps(_src, _0);
__m128 exp = _mm_or_ps(_mm_and_ps(_mm_rcp_ps(series), mask), _mm_andnot_ps(mask, series));
__m128 sigmoid = _mm_rcp_ps(_mm_add_ps(_1, exp));
_mm_storeu_ps(dst + i, sigmoid);
}
for (; i < size; ++i)
dst[i] = RoughSigmoid(src[i] * slope[0]);
}
Оптимизация функции RoughSigmoid с использованием AVX:
#include <immintrin.h>
void RoughSigmoid(const float * src, size_t size, const float * slope, float * dst)
{
size_t alignedSize = size/8*8;
__m256 _slope = _mm256_set1_ps(*slope);
__m256 _0 = _mm256_set1_ps(-0.0f);
__m256 _1 = _mm256_set1_ps(1.0f);
__m256 _0555 = _mm256_set1_ps(0.555f);
__m256 _0143 = _mm256_set1_ps(0.143f);
size_t i = 0;
for (; i < alignedSize; i += 8)
{
__m256 _src = _mm256_loadu_ps(src + i);
__m256 x = _mm256_andnot_ps(_0, _mm256_mul_ps(_src, _slope));
__m256 x2 = _mm256_mul_ps(x, x);
__m256 x4 = _mm256_mul_ps(x2, x2);
__m256 series = _mm256_add_ps(_mm256_add_ps(_1, x), _mm256_add_ps(_mm256_mul_ps(x2, _0555), _mm256_mul_ps(x4, _0143)));
__m256 mask = _mm256_cmp_ps(_src, _0, _CMP_GT_OS);
__m256 exp = _mm256_or_ps(_mm256_and_ps(_mm256_rcp_ps(series), mask), _mm256_andnot_ps(mask, series));
__m256 sigmoid = _mm256_rcp_ps(_mm256_add_ps(_1, exp));
_mm256_storeu_ps(dst + i, sigmoid);
}
for (; i < size; ++i)
dst[i] = RoughSigmoid(src[i] * slope[0]);
}
Используя Eureqa для поиска приближений к сигмоиду, я нашел, что 1/(1 + 0.3678749025^x)
приближает его. Это довольно близко, просто избавляется от одной операции с отрицанием x.
Некоторые из других функций, показанных здесь, интересны, но действительно ли операция питания медленна? Я тестировал его, и на самом деле это делалось быстрее, чем добавление, но это может быть просто случайностью. Если так, то это должно быть так же быстро или быстрее, как и все остальные.
EDIT: 0.5 + 0.5*tanh(0.5*x)
и менее точный, 0.5 + 0.5*tanh(n)
также работает. И вы можете просто избавиться от констант, если вам не нужно получать его между диапазоном [0,1], как сигмоид. Но он предполагает, что tanh быстрее.
Функция tanh может быть оптимизирована на некоторых языках, что делает ее быстрее, чем пользовательский определенный x/(1 + abs (x)), как это имеет место в Julia.
Я не думаю, что вы можете сделать лучше, чем встроенный exp(), но если вам нужен другой подход, вы можете использовать расширение серии. WolframAlpha может вычислить его для вас.