ニューラルネットワークアルゴリズムで実際の正確なシグモイド関数を使用する必要はありませんが、同様の特性を持っているが計算は高速な近似バージョンに置き換えることができます。
たとえば、「高速シグモイド」機能を使用できます
f(x) = x / (1 + abs(x))
Exp(x)の級数展開の最初の項を使用しても、f(x)の引数がゼロに近くなく、級数展開でも同じ問題がある場合は、あまり役に立ちません。引数が「大きい」場合のシグモイド関数の。
別の方法は、テーブル検索を使用することです。つまり、特定の数のデータポイントに対してシグモイド関数の値を事前に計算し、必要に応じてそれらの間の高速(線形)補間を行います。
最初にハードウェアで測定するのが最善です。簡単なベンチマーク script は、私のマシンでは1/(1+|x|)
が最速で、tanh(x)
が2番目に近いことを示しています。エラー関数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))
が高速パフォーマーである可能性があります。
ここの人々は、1つの関数が他の関数と比較してどれだけ速いかを主に懸念しており、f1(x)
がf2(x)
より0.0001ミリ秒速く実行されるかどうかを確認するマイクロベンチマークを作成します。大きな問題は、これがほとんど無関係であることです。重要なのは、コスト関数を最小化しようとするアクティベーション関数でネットワークがどれだけ速く学習するかです。
現在の理論では、 整流器関数とソフトプラス
シグモイド関数または類似の活性化関数と比較して、大規模で複雑なデータセットでのディープニューラルアーキテクチャの高速かつ効果的なトレーニングを可能にします。
そのため、マイクロ最適化を捨てて、どの関数がより高速な学習を可能にするかを調べることをお勧めします(他のさまざまなコスト関数も調べます)。
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);
}
SSEを使用したRoughSigmoid関数の最適化:
#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]);
}
AVXを使用したRoughSigmoid関数の最適化:
#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]);
}
次の2つの式を使用して、シンプルだが効果的な方法を使用できます。
if x < 0 then f(x) = 1 / (0.5/(1+(x^2)))
if x > 0 then f(x) = 1 / (-0.5/(1+(x^2)))+1
これは次のようになります。
シグモイドの2つのグラフ{青:(0.5 /(1+(x ^ 2))))、黄:(-0.5 /(1+(x ^ 2)))+ 1}
Eureqaを使用してシグモイドの近似を検索すると、1/(1 + 0.3678749025^x)
が近似することがわかりました。それはかなり近いです、xの否定で1つの操作を取り除くだけです。
ここに示されている他の機能のいくつかは興味深いですが、電源操作は本当に遅いですか?私はそれをテストし、実際には追加よりも速くしましたが、それは単なるまぐれかもしれません。もしそうなら、それは他のすべてと同じくらいまたはより速くなければなりません。
EDIT:0.5 + 0.5*tanh(0.5*x)
およびより正確ではない、0.5 + 0.5*tanh(n)
も機能します。また、シグモイドのように[0,1]の範囲で定数を取得する必要がない場合は、定数を削除できます。ただし、tanhの方が高速であると想定しています。
Tanh関数は一部の言語で最適化され、カスタム定義のx /(1 + abs(x))より高速になります。これはJuliaの場合です。