C# での数学的最適化
-
03-07-2019 - |
質問
私は一日中アプリケーションのプロファイリングを行っており、いくつかのコードを最適化したので、ToDo リストにこれが残っています。これはニューラル ネットワークの活性化関数であり、1 億回以上呼び出されます。dotTrace によると、これは全体の関数時間の約 60% に相当します。
これをどのように最適化しますか?
public static float Sigmoid(double value) {
return (float) (1.0 / (1.0 + Math.Pow(Math.E, -value)));
}
解決
試してください:
public static float Sigmoid(double value) {
return 1.0f / (1.0f + (float) Math.Exp(-value));
}
編集:簡単なベンチマークを行いました。私のマシンでは、上記のコードはメソッドよりも約43%高速であり、この数学的に同等のコードは、ほんの少しだけ高速です(元のコードより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);
}
また、何百万もの呼び出しを行っている場合、関数呼び出しのオーバーヘッドが問題になる可能性があります。インライン関数を作成してみて、それが助けになるかどうかを確認してください。
他のヒント
アクティベーション関数の場合、e ^ xの計算が完全に正確であるかどうかは非常に重要ですか?
たとえば、JavaでのPentiumテストで近似(1 + x / 256)^ 256を使用する場合(C#は本質的に同じプロセッサ命令にコンパイルされると仮定しています)、これは約7-8倍高速ですe ^ x(Math.exp())、および+/- 1.5の約xまでの小数点以下2桁まで正確で、指定した範囲全体で正しい桁数以内です。 (明らかに、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倍または半減(および乗算を追加/削除)します。 n = 4であっても、xの値に対して-0.5〜0.5の精度で小数点以下1.5桁の精度が得られます(Math.exp()よりも15倍高速です)。
PS言及するのを忘れました-明らかに256で本当に分割しないでください:定数1/256で乗算します。 JavaのJITコンパイラは、この最適化を自動的に行います(少なくとも、Hotspotは行います)。C#でも行う必要があると想定していました。
この投稿をご覧くださいa>。 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の場合)。この近似は、論文<!> quot; 指数関数の高速でコンパクトな近似 <!> quot;これは、ニューラルネットで使用するために開発されました。基本的には2048エントリのルックアップテーブルおよびエントリ間の線形近似と同じですが、これはすべてIEEE浮動小数点のトリックを使用しています。
編集: スペシャルソースによると、これは〜3.25倍高速ですCLRの実装。ありがとう!
- 覚えておいてください。 この活性化関数の変更には、異なる動作が伴います。. 。これには、float への切り替え (したがって精度の低下) やアクティベーション代替の使用も含まれます。自分のユースケースを試してみるだけで、正しい方法がわかります。
- 単純なコードの最適化に加えて、次のことも考慮することをお勧めします。 計算の並列化 (つまり:マシンの複数のコア、または Windows Azure クラウドのマシンを活用し、トレーニング アルゴリズムを改善します。
アップデート: ANN アクティベーション関数のルックアップ テーブルに投稿する
更新2: LUT を完全なハッシュと混同したため、LUT に関するポイントを削除しました。ありがとう ヘンリック・グスタフソン 私を軌道に戻してくれて。したがって、メモリは問題ではありませんが、探索空間は依然として局所的な極値によって少し混乱します。
1億回の呼び出しで、プロファイラーのオーバーヘッドが結果を歪めないのではないかと思うようになります。計算をノーオペレーションに置き換え、実行時間が60%を消費すると報告されているかまだ確認します...
さらに良いのは、テストデータをいくつか作成し、ストップウォッチタイマーを使用して100万件程度の呼び出しをプロファイルすることです。
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近くのコストがかかるため、一度に多くの小さなアレイを処理することをお勧めします。 10万個以上の要素を含むダブルバッファリングアプローチの使用を検討することもできます。
また、要素の数が大幅に増加し始めた場合、GPUでこれらの処理を選択することもできます(1D float4テクスチャを作成し、非常に簡単なフラグメントシェーダーを実行します)。
FWIW、これはすでに投稿された回答のC#ベンチマークです。 (Emptyは、関数呼び出しのオーバーヘッドを測定するために0を返す関数です)
Empty Function: 79ms 0 Original: 1576ms 0.7202294 Simplified: (soprano) 681ms 0.7202294 Approximate: (Neil) 441ms 0.7198783 Bit Manip: (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));
}
私の頭のてっぺんから、この論文は概算の方法を説明しています浮動小数点を悪用することによる指数関数(PDFの右上のリンクをクリックします)ですが、.NETでそれが非常に役立つかどうかはわかりません。
また、別のポイント:大規模なネットワークをすばやくトレーニングするために、使用しているロジスティックS字型はかなりひどいです。
F# は、.NET 数学アルゴリズムにおいて C# よりも優れたパフォーマンスを発揮します。 したがって、F# でニューラル ネットワークを書き直すと、全体的なパフォーマンスが向上する可能性があります。
再実装したら LUT ベンチマーク スニペット (私は少し調整したバージョンを使用しています) F# で、結果のコードは次のようになります。
- sigmoid1 ベンチマークを実行します 3899,2ms ではなく 588.8ms
- sigmoid2 (LUT) ベンチマークを実行します 411.4 ミリ秒ではなく 156.6 ミリ秒
詳細については、 ブログ投稿. 。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
アップデート: 10^7 の反復を使用して結果を C と同等にするようにベンチマークを更新しました
更新2: のパフォーマンス結果は次のとおりです。 Cの実装 同じマシンからのものと比較します。
Max deviation is 0.001664
10^7 iterations using sigmoid1: 628 ms
10^7 iterations using sigmoid2: 157 ms
注:これは、この投稿のフォローアップです。
編集: これおよび< href = "https://stackoverflow.com/questions/412019/code-optimizations#416180">これ、これ。
今、あなたが私にしたことを見てください! Monoをインストールさせてくれました!
$ 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倍速くなります。 Windowsボックスを持っている人は、MS-stuffを使用してメモリ使用量とパフォーマンスを調査できます:)
アクティベーション関数にLUTを使用することは、特にハードウェアに実装されている場合、それほど珍しいことではありません。これらのタイプのテーブルを含めることをいとわないのであれば、このコンセプトには多くの実証済みのバリエーションがあります。ただし、既に指摘したように、エイリアスは問題になる可能性がありますが、それを回避する方法もあります。さらに読む:
- ニューラルネットワークデジタル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());
}
}
最初に考えたのは、values変数の統計についてはどうですか?
- <!> quot; value <!> quot;の値は通常小さい-10 <!> lt; = value <!> lt; = 10?
そうでない場合、範囲外の値をテストすることにより、おそらくブーストを得ることができます
if(value < -10) return 0;
if(value > 10) return 1;
- 値は頻繁に繰り返されますか?
もしそうなら、おそらくメモ(おそらくそうではありませんが、確認するのに害はありません。...)
if(sigmoidCache.containsKey(value)) return sigmoidCache.get(value);
これらのどちらも適用できない場合は、他の人が示唆しているように、シグモイドの精度を下げることで解決できるかもしれません...
ソプラノはあなたの呼び出しにいくつかの素晴らしい最適化がありました:
public static float Sigmoid(double value)
{
float k = Math.Exp(value);
return k / (1.0f + k);
}
ルックアップテーブルを試してみて、使用するメモリが多すぎる場合、連続する呼び出しごとにパラメーターの値を常に確認し、キャッシュ手法を使用できます。
たとえば、最後の値と結果をキャッシュしてみてください。次の呼び出しが前の呼び出しと同じ値を持っている場合、最後の結果をキャッシュしたので、計算する必要はありません。現在の呼び出しが100回のうち1回でも前の呼び出しと同じであれば、100万回の計算を保存できる可能性があります。
または、連続する10回の呼び出し内で、値パラメーターが平均2回同じであることがわかる場合があるため、最後の10個の値/回答をキャッシュしてみてください。
アイデア:おそらく、値が事前に計算された(大きな)ルックアップテーブルを作成できますか?
これはトピックから少し外れていますが、好奇心から、 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実装(誰かが比較番号を投稿したい場合はJava 6)で結果を推測します違うでしょう。
コード:
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());
}
}
この質問が浮かび上がってから1年が経ちましたが、C#と比較したF#とCのパフォーマンスについての議論のため、この問題に出くわしました。他のレスポンダーのサンプルをいくつか試してみたところ、デリゲートは通常のメソッド呼び出しよりも速く実行されるように見えますが、 C#よりもF#に明らかなパフォーマンス上の利点はありません。
- C:166ms
- C#(デリゲート):275ms
- C#(メソッド):431ms
- C#(メソッド、フロートカウンター):2,656ms
- F#:404ms
フロートカウンターを備えたC#は、Cコードのストレートポートでした。 forループでintを使用する方がはるかに高速です。
また、評価が安価な代替アクティベーション関数を試すことも検討できます。例:
f(x) = (3x - x**3)/2
(これは次のようにファクタリングできます
f(x) = x*(3 - x*x)/2
1つ少ない乗算用)。この関数の対称性は奇数であり、その導関数は自明です。ニューラルネットワークに使用するには、入力の総数で除算することにより入力の合計を正規化する必要があります(ドメインも[-1..1]に制限されます。これも範囲です)。
ソプラノのテーマの軽微なバリエーション:
public static float Sigmoid(double value) {
float v = value;
float k = Math.Exp(v);
return k / (1.0f + k);
}
単精度の結果を求めているのに、なぜMath.Exp関数でdoubleを計算するのですか?反復合計を使用する指数計算機( e x )を使用すると、毎回より正確に時間がかかります。また、doubleはsingleの2倍です!この方法では、最初にシングルに変換し、 then 指数関数を実行します。
しかし、expf関数はさらに高速になるはずです。ただし、C#が暗黙のfloat-double変換を行わない限り、ソプラノ(float)をexpfに渡す必要はありません。
それ以外の場合は、FORTRANなどの real 言語を使用してください...
ここには良い答えがたくさんあります。実行することをお勧めします このテクニック, 、 念のため
- 必要以上にそれを呼び出す必要はありません。
(関数は非常に簡単に呼び出せるという理由だけで、必要以上に呼び出されることもあります。) - 同じ引数で繰り返し呼び出していない
(メモ化を使用できる場合)
ところで、あなたが持っている関数は逆ロジット関数です、
または対数オッズ比関数の逆関数 log(f/(1-f))
.
(パフォーマンス測定で更新)(実際の結果で再度更新:)
ルックアップテーブルソリューションは、パフォーマンスに関して言えば、ごくわずかなメモリと精度のコストで、はるかに遠くまで到達すると思います。
次のスニペットは、Cでの実装例です(ドライコードを作成するのに十分なc#を流speakに話せません)。十分に動作し、十分に動作しますが、バグがあると確信しています:)
#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
TODO:
改善すべき点と弱点を取り除く方法があります。方法は読者の課題として残されています:)
- 関数の範囲を調整して、テーブルが開始および終了するジャンプを回避します。
- わずかなノイズ関数を追加して、エイリアシングアーティファクトを隠します。
- Rexが言ったように、補間を使用すると、精度がかなり向上し、パフォーマンスがかなり低下します。
非常に類似したことを行うはるかに高速な関数があります:
x / (1 + abs(x))
<!>#8211; TAHNの高速交換
そして同様に:
x / (2 + 2 * abs(x)) + 0.5
-SIGMOIDの高速置換
実際のシグモイドするでプロットを比較。
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
1)1つの場所からのみこれを呼び出しますか?その場合、コードをその関数の外に移動し、通常Sigmoid関数を呼び出す場所に配置するだけで、わずかなパフォーマンスを得ることができます。コードの読みやすさと編成の点ではこの考えは好きではありませんが、最後のパフォーマンス向上を得る必要がある場合、これは役立つかもしれません。コードはすべてインラインでした。
2)これが役立つかどうかはわかりませんが、関数パラメーターをrefパラメーターにしてみてください。より高速かどうかを確認します。 constにすることをお勧めします(これがc ++の場合は最適化されます)が、c#はconstパラメーターをサポートしていません。
巨大な速度向上が必要な場合は、おそらく(ge)forceを使用して関数の並列化を検討することができます。 IOW、DirectXを使用してグラフィックカードを制御し、それを実行します。これを行う方法はわかりませんが、あらゆる種類の計算にグラフィックカードを使用する人を見てきました。
この辺りの多くの人が近似を使ってシグモイドを高速化しようとしているのを見てきました。ただし、シグモイドはexpだけでなくtanhを使用しても表現できることを知っておくことが重要です。 この方法でSigmoidを計算すると、指数関数を使用した場合よりも約5倍高速になり、この方法を使用しても近似は行われないため、Sigmoidの元の動作はそのまま維持されます。
public static double Sigmoid(double value)
{
return 0.5d + 0.5d * Math.Tanh(value/2);
}
もちろん、並列化はパフォーマンス改善の次のステップですが、生の計算に関する限り、Math.Tanhの使用はMath.Exp。よりも高速です。