web-dev-qa-db-ja.com

C#での数学の最適化

私は1日中アプリケーションのプロファイリングを行っており、2、3のコードを最適化したので、これをtodoリストに残しました。 1億回以上呼び出されるニューラルネットワークのアクティベーション関数です。 dotTraceによると、これは全体の関数時間の約60%に相当します。

これをどのように最適化しますか?

public static float Sigmoid(double value) {
    return (float) (1.0 / (1.0 + Math.Pow(Math.E, -value)));
}
56
hb.

試してください:

public static float Sigmoid(double value) {
    return 1.0f / (1.0f + (float) Math.Exp(-value));
}

編集:簡単なベンチマークを行いました。私のマシンでは、上記のコードはメソッドよりも約43%高速であり、この数学的に同等のコードは10分の1だけ高速です(オリジナルよりも46%高速)。

public static float Sigmoid(double value) {
    float k = Math.Exp(value);
    return k / (1.0f + k);
}

編集2: C#関数のオーバーヘッドの量はわかりませんが、#include <math.h>ソースコードでは、float-exp関数を使用してこれを使用できるはずです。少し速いかもしれません。

public static float Sigmoid(double value) {
    float k = expf((float) value);
    return k / (1.0f + k);
}

また、何百万もの呼び出しを行っている場合は、関数呼び出しのオーバーヘッドが問題になることがあります。インライン関数を作成してみて、それが役立つかどうかを確認してください。

55
Sophie Alpert

アクティベーション関数の場合、e ^ xの計算が完全に正確であるかどうかは非常に重要ですか?

たとえば、近似値(1 + x/256)^ 256を使用する場合、PentiumテストでJava(私はC#が基本的に同じプロセッサ命令にコンパイルされると仮定しています)でこれは約ですe ^ x(Math.exp())の7〜8倍の速さで、小数点以下2桁まで、約xまで+/- 1.5の精度で、指定した範囲全体で正しい桁の範囲内です(明らかに、 256にレイズするには、実際に数値を8乗する-これにはMath.Powを使用しないでください!)Javaの場合:

double eapprox = (1d + x / 256d);
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;

近似をどの程度正確にしたいかに応じて、256を2倍または2倍にします(そして乗算を追加/削除します)。 n = 4であっても、xの値が-0.5と0.5の間で小数点以下約1.5桁の精度になります(Math.exp()よりも15倍高速に表示されます)。

追伸私は言及するのを忘れました-あなたは明らかにすべきではありません 本当に 256で割ります:定数1/256を掛けます。 JavaのJITコンパイラーはこの最適化を自動的に行い(少なくとも、Hotspotは行います)、私はC#も行う必要があると想定していました。

30
Neil Coffey

この投稿 をご覧ください。 Javaで記述されたe ^ xの近似があり、これはそのC#コードである必要があります(テストされていません)。

public static double Exp(double val) {  
    long tmp = (long) (1512775 * val + 1072632447);  
    return BitConverter.Int64BitsToDouble(tmp << 32);  
}

私のベンチマークでは、これはMath.exp()の5倍(Javaの場合)を超えています。この近似は、ニューラルネットで使用するために開発された論文「 指数関数の高速でコンパクトな近似 」に基づいています。これは基本的に2048エントリのルックアップテーブルとエントリ間の線形近似と同じですが、これらはすべてIEEE浮動小数点トリックを使用しています。

編集:によると Special Sauce これはCLR実装より〜3.25倍高速です。ありがとう!

22
martinus
  1. このアクティベーション関数の変更は、異なる動作を犠牲にして行われることに注意してください。これには、フロートへの切り替え(つまり、精度の低下)またはアクティベーションの代用の使用も含まれます。ユースケースを試してみることだけが正しい方法を示します。
  2. 単純なコード最適化に加えて、計算の並列化を検討することもお勧めします(つまり、マシンの複数のコアまたはWindows Azureクラウド)とトレーニングアルゴリズムの改善。

UPDATE:ANNアクティベーション関数のルックアップテーブルに投稿

UPDATE2:これらを完全なハッシュと混同していたため、LUTのポイントを削除しました。 Henrik Gustafsson に戻ってくれてありがとう。したがって、メモリは問題ではありませんが、検索スペースは依然としてローカル極値に少し混乱しています。

14
Rinat Abdullin

1億回の呼び出しで、プロファイラーのオーバーヘッドが結果を歪ませていないのではないかと考え始めます。計算をno-opに置き換え、それがstillであるかどうかを確認します。実行時間の60%を消費すると報告されています...

あるいは、テストデータをいくつか作成し、ストップウォッチタイマーを使用して、100万程度の通話のプロファイルを作成します。

8
Shog9

C++と相互運用できる場合は、すべての値を配列に格納して、SSE=

void sigmoid_sse(float *a_Values, float *a_Output, size_t a_Size){
    __m128* l_Output = (__m128*)a_Output;
    __m128* l_Start  = (__m128*)a_Values;
    __m128* l_End    = (__m128*)(a_Values + a_Size);

    const __m128 l_One        = _mm_set_ps1(1.f);
    const __m128 l_Half       = _mm_set_ps1(1.f / 2.f);
    const __m128 l_OneOver6   = _mm_set_ps1(1.f / 6.f);
    const __m128 l_OneOver24  = _mm_set_ps1(1.f / 24.f);
    const __m128 l_OneOver120 = _mm_set_ps1(1.f / 120.f);
    const __m128 l_OneOver720 = _mm_set_ps1(1.f / 720.f);
    const __m128 l_MinOne     = _mm_set_ps1(-1.f);

    for(__m128 *i = l_Start; i < l_End; i++){
        // 1.0 / (1.0 + Math.Pow(Math.E, -value))
        // 1.0 / (1.0 + Math.Exp(-value))

        // value = *i so we need -value
        __m128 value = _mm_mul_ps(l_MinOne, *i);

        // exp expressed as inifite series 1 + x + (x ^ 2 / 2!) + (x ^ 3 / 3!) ...
        __m128 x = value;

        // result in l_Exp
        __m128 l_Exp = l_One; // = 1

        l_Exp = _mm_add_ps(l_Exp, x); // += x

        x = _mm_mul_ps(x, x); // = x ^ 2
        l_Exp = _mm_add_ps(l_Exp, _mm_mul_ps(l_Half, x)); // += (x ^ 2 * (1 / 2))

        x = _mm_mul_ps(value, x); // = x ^ 3
        l_Exp = _mm_add_ps(l_Exp, _mm_mul_ps(l_OneOver6, x)); // += (x ^ 3 * (1 / 6))

        x = _mm_mul_ps(value, x); // = x ^ 4
        l_Exp = _mm_add_ps(l_Exp, _mm_mul_ps(l_OneOver24, x)); // += (x ^ 4 * (1 / 24))

#ifdef MORE_ACCURATE

        x = _mm_mul_ps(value, x); // = x ^ 5
        l_Exp = _mm_add_ps(l_Exp, _mm_mul_ps(l_OneOver120, x)); // += (x ^ 5 * (1 / 120))

        x = _mm_mul_ps(value, x); // = x ^ 6
        l_Exp = _mm_add_ps(l_Exp, _mm_mul_ps(l_OneOver720, x)); // += (x ^ 6 * (1 / 720))

#endif

        // we've calculated exp of -i
        // now we only need to do the '1.0 / (1.0 + ...' part
        *l_Output++ = _mm_rcp_ps(_mm_add_ps(l_One,  l_Exp));
    }
}

ただし、使用する配列は、_aligned_malloc(some_size * sizeof(float)、16)を使用して割り当てる必要があることに注意してください。これは、SSEは、境界に合わせてメモリを配置する必要があるためです。

SSEを使用すると、1億個すべての要素の結果を約0.5秒で計算できます。ただし、一度に多くのメモリを割り当てると、ギガバイトの約3分の2のコストがかかるため、一度に処理する配列の数を増やすことをお勧めします。 100K要素以上のダブルバッファリングアプローチの使用を検討することもできます。

また、要素の数が大幅に増加し始めた場合は、GPUでこれらの処理を選択することをお勧めします(1D float4テクスチャを作成し、非常に簡単なフラグメントシェーダーを実行するだけです)。

8
Jasper Bekkers

FWIW、これはすでに投稿された回答の私のC#ベンチマークです。 (空は、関数呼び出しのオーバーヘッドを測定するために、単に0を返す関数です)

空関数:79ms 0 
元:1576ms 0.7202294 
簡略化:(ソプラノ)681ms 0.7202294 
概算:(ニール)441ms 0.7198783 
ビット操作:(martinus)836ms 0.72318 
 Taylor:(Rex Logan)261ms 0.7202305 
 Lookup:(Henrik)182ms 0.7204863 
public static object[] Time(Func<double, float> f) {
    var testvalue = 0.9456;
    var sw = new Stopwatch();
    sw.Start();
    for (int i = 0; i < 1e7; i++)
        f(testvalue);
    return new object[] { sw.ElapsedMilliseconds, f(testvalue) };
}
public static void Main(string[] args) {
    Console.WriteLine("Empty:       {0,10}ms {1}", Time(Empty));
    Console.WriteLine("Original:    {0,10}ms {1}", Time(Original));
    Console.WriteLine("Simplified:  {0,10}ms {1}", Time(Simplified));
    Console.WriteLine("Approximate: {0,10}ms {1}", Time(ExpApproximation));
    Console.WriteLine("Bit Manip:   {0,10}ms {1}", Time(BitBashing));
    Console.WriteLine("Taylor:      {0,10}ms {1}", Time(TaylorExpansion));
    Console.WriteLine("Lookup:      {0,10}ms {1}", Time(LUT));
}
7
Jimmy

F#は.NET数学アルゴリズムでC#よりもパフォーマンスが優れています。したがって、F#でニューラルネットワークを書き換えると、全体的なパフォーマンスが向上する可能性があります。

F#で LUTベンチマークスニペット (少し微調整したバージョンを使用しています)を再実装すると、結果のコードは次のようになります。

  • 3899,2ms(---)ではなく588.8msでsigmoid1ベンチマークを実行します
  • sigmoid2(LUT)ベンチマークを411.4 msではなく156.6msで実行します

詳細は ブログ投稿 にあります。 F#スニペットJICは次のとおりです。

#light

let Scale = 320.0f;
let Resolution = 2047;

let Min = -single(Resolution)/Scale;
let Max = single(Resolution)/Scale;

let range step a b =
  let count = int((b-a)/step);
  seq { for i in 0 .. count -> single(i)*step + a };

let lut = [| 
  for x in 0 .. Resolution ->
    single(1.0/(1.0 +  exp(-double(x)/double(Scale))))
  |]

let sigmoid1 value = 1.0f/(1.0f + exp(-value));

let sigmoid2 v = 
  if (v <= Min) then 0.0f;
  Elif (v>= Max) then 1.0f;
  else
    let f = v * Scale;
    if (v>0.0f) then lut.[int (f + 0.5f)]
    else 1.0f - lut.[int(0.5f - f)];

let getError f = 
  let test = range 0.00001f -10.0f 10.0f;
  let errors = seq { 
    for v in test -> 
      abs(sigmoid1(single(v)) - f(single(v)))
  }
  Seq.max errors;

open System.Diagnostics;

let test f = 
  let sw = Stopwatch.StartNew(); 
  let mutable m = 0.0f;
  let result = 
    for t in 1 .. 10 do
      for x in 1 .. 1000000 do
        m <- f(single(x)/100000.0f-5.0f);
  sw.Elapsed.TotalMilliseconds;

printf "Max deviation is %f\n" (getError sigmoid2)
printf "10^7 iterations using sigmoid1: %f ms\n" (test sigmoid1)
printf "10^7 iterations using sigmoid2: %f ms\n" (test sigmoid2)

let c = System.Console.ReadKey(true);

そして、出力(デバッガーなしのF#1.9.6.2 CTPに対するコンパイルのリリース):

Max deviation is 0.001664
10^7 iterations using sigmoid1: 588.843700 ms
10^7 iterations using sigmoid2: 156.626700 ms

UPDATE:10 ^ 7回の反復を使用して結果をCに匹敵するようにベンチマークを更新

UPDATE2:これは、比較対象の同じマシンからの C実装 のパフォーマンス結果です。

Max deviation is 0.001664
10^7 iterations using sigmoid1: 628 ms
10^7 iterations using sigmoid2: 157 ms
5
Rinat Abdullin

注:これは this 投稿のフォローアップです。

編集:this および this と同じものを計算するように更新し、- これ

今あなたが私にさせたものを見てください!あなたは私にモノをインストールさせました!

$ gmcs -optimize test.cs && mono test.exe
Max deviation is 0.001663983
10^7 iterations using Sigmoid1() took 1646.613 ms
10^7 iterations using Sigmoid2() took 237.352 ms

Cはもう努力する価値はほとんどありません。世界は前進しています:)

だから、ちょうど因子を超えて 10 6より高速です。ウィンドウボックスを持っている人は、MSのものを使用してメモリ使用量とパフォーマンスを調査します:)

アクティベーション機能にLUTを使用することは、特にハードウェアに実装されている場合はそれほど珍しくありません。これらのタイプのテーブルを含めることをいとわないのであれば、コンセプトのさまざまな実証済みのバリエーションがあります。ただし、すでに指摘したように、エイリアシングが問題になる可能性がありますが、それを回避する方法もあります。さらに読む:

これに関するいくつかの落とし穴:

  • テーブルの外に到達するとエラーが発生します(ただし、極端な場合は0に収束します)。 x約+ -7.0の場合。これは、選択したスケーリング係数が原因です。 SCALEの値が大きいほど、中央の範囲では誤差が大きくなりますが、エッジでは誤差が小さくなります。
  • これは一般的に非常に愚かなテストであり、私はC#を知りません、それは私のCコードの単なる変換です:)
  • Rinat Abdullin は非常に正しいので、エイリアシングと精度の損失によって問題が発生する可能性があります。実際、私はルックアップテーブルの問題を除いて、彼が言うすべてに同意します。

コピーペーストのコーディングを許してください...

using System;
using System.Diagnostics;

class LUTTest {
    private const float SCALE = 320.0f;
    private const int RESOLUTION = 2047;
    private const float MIN = -RESOLUTION / SCALE;
    private const float MAX = RESOLUTION / SCALE;

    private static readonly float[] lut = InitLUT();

    private static float[] InitLUT() {
      var lut = new float[RESOLUTION + 1];

      for (int i = 0; i < RESOLUTION + 1; i++) {
        lut[i] = (float)(1.0 / (1.0 + Math.Exp(-i / SCALE)));
      }
      return lut;
    }

    public static float Sigmoid1(double value) {
        return (float) (1.0 / (1.0 + Math.Exp(-value)));
    }

    public static float Sigmoid2(float value) {
      if (value <= MIN) return 0.0f;
      if (value >= MAX) return 1.0f;
      if (value >= 0) return lut[(int)(value * SCALE + 0.5f)];
      return 1.0f - lut[(int)(-value * SCALE + 0.5f)];
    }

    public static float error(float v0, float v1) {
      return Math.Abs(v1 - v0);
    }

    public static float TestError() {
        float emax = 0.0f;
        for (float x = -10.0f; x < 10.0f; x+= 0.00001f) {
          float v0 = Sigmoid1(x);
          float v1 = Sigmoid2(x);
          float e = error(v0, v1);
          if (e > emax) emax = e;
        }
        return emax;
    }

    public static double TestPerformancePlain() {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        for (int i = 0; i < 10; i++) {
            for (float x = -5.0f; x < 5.0f; x+= 0.00001f) {
                Sigmoid1(x);
            }
        }
        sw.Stop();
        return sw.Elapsed.TotalMilliseconds;
    }    

    public static double TestPerformanceLUT() {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        for (int i = 0; i < 10; i++) {
            for (float x = -5.0f; x < 5.0f; x+= 0.00001f) {
                Sigmoid2(x);
            }
        }
        sw.Stop();
        return sw.Elapsed.TotalMilliseconds;
    }    

    static void Main() {
        Console.WriteLine("Max deviation is {0}", TestError());
        Console.WriteLine("10^7 iterations using Sigmoid1() took {0} ms", TestPerformancePlain());
        Console.WriteLine("10^7 iterations using Sigmoid2() took {0} ms", TestPerformanceLUT());
    }
}
5

ソプラノはあなたの呼び出しにいくつかの素晴らしい最適化を持っていました:

public static float Sigmoid(double value) 
{
    float k = Math.Exp(value);
    return k / (1.0f + k);
}

ルックアップテーブルを試してみて、メモリの使用量が多すぎることがわかった場合は、連続する呼び出しごとに常にパラメータの値を調べて、キャッシュテクニックを使用することができます。

たとえば、最後の値と結果をキャッシュしてみます。次の呼び出しの値が前の呼び出しと同じ場合、最後の結果をキャッシュしているので、計算する必要はありません。現在の呼び出しが100回に1回でも前の呼び出しと同じだった場合、100万回の計算を節約できる可能性があります。

または、連続する10回の呼び出し内で、値パラメーターが平均して2回同じであるため、最後の10個の値/回答をキャッシュしてみることができます。

4
Jeremy

私の頭の上から このペーパーでは、浮動小数点を悪用して指数を概算する方法を説明しています 、(PDFの右上にあるリンクをクリックします)しかし、それが可能かどうかはわかりません.NETで非常に役立ちます。

また、別のポイント:大規模なネットワークをすばやくトレーニングするために、使用しているロジスティックシグモイドはかなりひどいです。 LeCun et alによるEfficient Backprop のセクション4.4を参照し、ゼロを中心とするものを使用してください(実際には、論文全体を読んでください。非常に便利です)。

4
dwf

最初に考えた:値の変数に関するいくつかの統計はどうですか?

  • 「値」の値は通常-10 <=値<= 10と小さいですか?

そうでない場合は、範囲外の値をテストすることにより、おそらくブーストを得ることができます

if(value < -10)  return 0;
if(value > 10)  return 1;
  • 値は頻繁に繰り返されますか?

もしそうなら、おそらく メモ化 からいくつかの利益を得ることができます(おそらくそうではありませんが、チェックするのに害はありません...)。

if(sigmoidCache.containsKey(value)) return sigmoidCache.get(value);

これらのいずれも適用できない場合は、他の人が示唆しているように、シグモイドの精度を下げることで回避できるかもしれません...

4
Stobor

アイデア:おそらく、値を事前に計算して(大きな)ルックアップテーブルを作成できますか?

2
Vilx-

これは少し外れたトピックですが、好奇心から、 [〜#〜] c [〜#〜]C# および-の実装と同じ実装を行いました F# Java。他の誰かが好奇心をそそる場合に備えて、ここにはそのままにしておきます。

結果:

$ javac LUTTest.Java && Java LUTTest
Max deviation is 0.001664
10^7 iterations using sigmoid1() took 1398 ms
10^7 iterations using sigmoid2() took 177 ms

私の場合のC#の改善は、JavaがOS XのMonoよりも最適化されているためだと思います。同様のMS .NET実装(vs. Javaでは、比較番号を投稿したい場合)結果は異なると思います。

コード:

public class LUTTest {
    private static final float SCALE = 320.0f;
    private static final  int RESOLUTION = 2047;
    private static final  float MIN = -RESOLUTION / SCALE;
    private static final  float MAX = RESOLUTION / SCALE;

    private static final float[] lut = initLUT();

    private static float[] initLUT() {
        float[] lut = new float[RESOLUTION + 1];

        for (int i = 0; i < RESOLUTION + 1; i++) {
            lut[i] = (float)(1.0 / (1.0 + Math.exp(-i / SCALE)));
        }
        return lut;
    }

    public static float sigmoid1(double value) {
        return (float) (1.0 / (1.0 + Math.exp(-value)));
    }

    public static float sigmoid2(float value) {
        if (value <= MIN) return 0.0f;
        if (value >= MAX) return 1.0f;
        if (value >= 0) return lut[(int)(value * SCALE + 0.5f)];
        return 1.0f - lut[(int)(-value * SCALE + 0.5f)];
    }

    public static float error(float v0, float v1) {
        return Math.abs(v1 - v0);
    }

    public static float testError() {
        float emax = 0.0f;
        for (float x = -10.0f; x < 10.0f; x+= 0.00001f) {
            float v0 = sigmoid1(x);
            float v1 = sigmoid2(x);
            float e = error(v0, v1);
            if (e > emax) emax = e;
        }
        return emax;
    }

    public static long sigmoid1Perf() {
        float y = 0.0f;
        long t0 = System.currentTimeMillis();
        for (int i = 0; i < 10; i++) {
            for (float x = -5.0f; x < 5.0f; x+= 0.00001f) {
                y = sigmoid1(x);
            }
        }
        long t1 = System.currentTimeMillis();
        System.out.printf("",y);
        return t1 - t0;
    }    

    public static long sigmoid2Perf() {
        float y = 0.0f;
        long t0 = System.currentTimeMillis();
        for (int i = 0; i < 10; i++) {
            for (float x = -5.0f; x < 5.0f; x+= 0.00001f) {
                y = sigmoid2(x);
            }
        }
        long t1 = System.currentTimeMillis();
        System.out.printf("",y);
        return t1 - t0;
    }    

    public static void main(String[] args) {

        System.out.printf("Max deviation is %f\n", testError());
        System.out.printf("10^7 iterations using sigmoid1() took %d ms\n", sigmoid1Perf());
        System.out.printf("10^7 iterations using sigmoid2() took %d ms\n", sigmoid2Perf());
    }
}
2

この質問が表示されてから1年が経過していることを理解していますが、C#と比較したF#およびCのパフォーマンスについての議論のため、私はそれに遭遇しました。他のレスポンダーのサンプルをいくつか試してみたところ、デリゲートが通常のメソッド呼び出しよりも高速に実行されているように見えますが、 C#よりもF#の方がパフォーマンス上の明らかな利点はありません

  • C:166ミリ秒
  • C#(デリゲート):275ミリ秒
  • C#(メソッド):431ms
  • C#(メソッド、フロートカウンター):2,656ms
  • F#:404ミリ秒

Floatカウンターを備えたC#は、Cコードのストレートポートでした。 forループでintを使用する方がはるかに高速です。

2
Brian Reiter

(パフォーマンス測定で更新)(実際の結果で再度更新:)

パフォーマンスに関しては、ルックアップテーブルソリューションを使用すると、メモリと精度のコストを無視して、非常に遠くまで行けると思います。

次のスニペットは、Cでの実装例です(私はc#を十分に流暢に話せず、ドライコーディングできません)。実行して十分に機能しますが、バグがあると確信しています:)

#include <math.h>
#include <stdio.h>
#include <time.h>

#define SCALE 320.0f
#define RESOLUTION 2047
#define MIN -RESOLUTION / SCALE
#define MAX RESOLUTION / SCALE

static float sigmoid_lut[RESOLUTION + 1];

void init_sigmoid_lut(void) {
    int i;    
    for (i = 0; i < RESOLUTION + 1; i++) {
        sigmoid_lut[i] =  (1.0 / (1.0 + exp(-i / SCALE)));
    }
}

static float sigmoid1(const float value) {
    return (1.0f / (1.0f + expf(-value)));
}

static float sigmoid2(const float value) {
    if (value <= MIN) return 0.0f;
    if (value >= MAX) return 1.0f;
    if (value >= 0) return sigmoid_lut[(int)(value * SCALE + 0.5f)];
    return 1.0f-sigmoid_lut[(int)(-value * SCALE + 0.5f)];
}

float test_error() {
    float x;
    float emax = 0.0;

    for (x = -10.0f; x < 10.0f; x+=0.00001f) {
        float v0 = sigmoid1(x);
        float v1 = sigmoid2(x);
        float error = fabsf(v1 - v0);
        if (error > emax) { emax = error; }
    } 
    return emax;
}

int sigmoid1_perf() {
    clock_t t0, t1;
    int i;
    float x, y = 0.0f;

    t0 = clock();
    for (i = 0; i < 10; i++) {
        for (x = -5.0f; x <= 5.0f; x+=0.00001f) {
            y = sigmoid1(x);
        }
    }
    t1 = clock();
    printf("", y); /* To avoid sigmoidX() calls being optimized away */
    return (t1 - t0) / (CLOCKS_PER_SEC / 1000);
}

int sigmoid2_perf() {
    clock_t t0, t1;
    int i;
    float x, y = 0.0f;
    t0 = clock();
    for (i = 0; i < 10; i++) {
        for (x = -5.0f; x <= 5.0f; x+=0.00001f) {
            y = sigmoid2(x);
        }
    }
    t1 = clock();
    printf("", y); /* To avoid sigmoidX() calls being optimized away */
    return (t1 - t0) / (CLOCKS_PER_SEC / 1000);
}

int main(void) {
    init_sigmoid_lut();
    printf("Max deviation is %0.6f\n", test_error());
    printf("10^7 iterations using sigmoid1: %d ms\n", sigmoid1_perf());
    printf("10^7 iterations using sigmoid2: %d ms\n", sigmoid2_perf());

    return 0;
}

以前の結果は、オプティマイザがその仕事をし、計算を最適化するためでした。実際にコードを実行させると、少し異なる、はるかに興味深い結果が得られます(途中でMB Airが遅くなります)。

$ gcc -O2 test.c -o test && ./test
Max deviation is 0.001664
10^7 iterations using sigmoid1: 571 ms
10^7 iterations using sigmoid2: 113 ms

profile


TODO:

改善すべき点と弱点を取り除く方法があります。方法は読者への練習問題として残されています:)

  • 関数の範囲を調整して、テーブルが開始および終了するジャンプを回避します。
  • わずかなノイズ関数を追加して、エイリアシングアーティファクトを非表示にします。
  • Rexが言ったように、補間は、パフォーマンスに関してはかなり安価でありながら、精度に関してはかなりの更なるメリットをもたらします。
1

また、評価が安価な代替アクティベーション関数を試すことを検討することもできます。例えば:

f(x) = (3x - x**3)/2

(これは、

f(x) = x*(3 - x*x)/2

乗算が1つ減ります)。この関数は奇妙な対称性を持ち、その導関数は自明です。ニューラルネットワークに使用するには、入力の合計数で領域を分割することにより、入力の合計を正規化する必要があります(ドメインを[-1..1]に制限します。これも範囲です)。

1
joel.neely

ここには良い答えがたくさんあります。 このテクニック で実行することをお勧めします

  • あなたは必要以上にそれを呼び出すことはありません。
    (関数が非常に簡単に呼び出せるからといって、関数が必要以上に呼び出されることがあります。)
  • 同じ引数で繰り返し呼び出すのではありません
    (メモを使用できる場所)

ところであなたが持っている関数は逆ロジット関数です、
またはlog-odds-ratio関数の逆関数log(f/(1-f))

1
Mike Dunlavey

非常によく似た処理を行う非常に高速な関数があります。

x / (1 + abs(x)) – TAHNの高速置換

そして同様に:

x / (2 + 2 * abs(x)) + 0.5-SIGMOIDの高速置換

プロットを実際のシグモイドと比較

1
Lex

ソプラノのテーマの穏やかなバリエーション:

public static float Sigmoid(double value) {
    float v = value;
    float k = Math.Exp(v);
    return k / (1.0f + k);
}

単精度の結果しか得られないので、Math.Exp関数でdoubleを計算するのはなぜですか?反復合計を使用する指数計算機( eの展開を参照)バツ )の精度を上げるには、毎回時間がかかります。そしてダブルはシングルの2倍の仕事です!この方法では、最初にシングルに変換し、 その後 あなたの指数を行います。

しかし、expf関数はさらに高速である必要があります。 C#が暗黙のfloat-double変換を行わない限り、ソプラノの(float)キャストがexpfに渡される必要はないと思います。

それ以外の場合は、 リアル FORTRANのような言語...

1
Phil H

1)これを1か所だけから呼び出しますか?その場合、コードをその関数の外に移動し、通常はSigmoid関数を呼び出した場所にコードを配置するだけで、わずかなパフォーマンスを得ることができます。コードの読みやすさと構成の点でこのアイデアは好きではありませんが、最後のすべてのパフォーマンスを向上させる必要がある場合は、関数呼び出しでスタックにレジスタのプッシュ/ポップが必要になるため、これが役立つ場合があります。コードはすべてインラインでした。

2)これが役立つかどうかはわかりませんが、関数パラメーターを参照パラメーターにしてみてください。それが速いかどうかを確認します。私はそれをconstにすることを提案しましたが(これがc ++の場合は最適化されていました)、c#はconstパラメータをサポートしていません。

0
Jeremy

巨大な速度向上が必要な場合は、(ge)forceを使用して関数を並列化することを検討してください。 IOW、DirectXを使用してグラフィックカードを制御します。これを行う方法はわかりませんが、人々があらゆる種類の計算にグラフィックスカードを使用するのを見てきました。

0
erikkallen

Google検索を行ったところ、Sigmoid関数の代替実装が見つかりました。

public double Sigmoid(double x)
{
   return 2 / (1 + Math.Exp(-2 * x)) - 1;
}

それはあなたのニーズに合っていますか?速いですか?

http://dynamicnotions.blogspot.com/2008/09/sigmoid-function-in-c.html

0
Haacked

この辺りの多くの人が近似を使ってシグモイドを高速化しようとしているのを見てきました。ただし、シグモイドは、expだけでなく、tanhを使用しても表現できることを知っておくことが重要です。この方法でシグモイドを計算すると、指数関数を使用した場合よりも約5倍高速になり、この方法を使用しても何も近似されないため、シグモイドの元の動作はそのまま維持されます。

    public static double Sigmoid(double value)
    {
        return 0.5d + 0.5d * Math.Tanh(value/2);
    }

もちろん、並列化はパフォーマンス改善の次のステップになりますが、生の計算に関しては、Math.Tanhを使用する方がMath.Expよりも高速です。

0
Dash