在C#中进行math优化

我一整天都在分析一个应用程序,并且优化了一些代码,我把它留在了我的待办事项列表中。 这是一个neural network的激活function,被称为超过1亿次。 根据dotTrace,它占总function时间的大约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%,这个math上等价的代码是最快的(比原来快了46%):

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

编辑2:我不知道C#函数有多less开销,但是如果你在你的源代码中包含#include <math.h> ,你应该可以使用这个,它使用了一个float-exp函数。 它可能会快一点。

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

此外,如果您正在执行数百万次调用,则函数调用开销可能会成为问题。 尝试做一个内联函数,看看是否有任何帮助。

如果是激活函数,那么e ^ x的计算是否完全准确呢?

例如,如果你使用近似值(1 + x / 256)^ 256,在我的Pentiumtesting中(我假设C#本质上编译为相同的处理器指令),这比e ^ x快大约7-8倍(Math.exp()),并精确到小数点后两位,高达+/- 1.5的x,并且在所述范围内的正确数量级内。 (显然,要提高到256,你实际上把这个数字平方八次 – 不要用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(以及添加/去除乘法),取决于您希望逼近的准确程度。 即使n = 4,对于x的值在-0.5和0.5之间仍然会给出1.5个小数位的精确度(并且看起来比Math.exp()快15倍)。

PS我忘了提 – 你应该显然不是真正的256分:乘以恒定的1/256。 Java的JIT编译器会自动进行这种优化(至less,Hotspot是这样做的),我假定C#也必须这样做。

看看这个post 。 它用Java编写的e ^ x近似值,这应该是C#代码(未经testing):

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

在我的基准testing中,这比Math.exp() (在Java中) 快5倍以上 。 近似是基于“ A Fast,Compact Approximation of the Exponential Function ”的论文,它正是为了neural network而开发的。 它基本上与2048条目的查找表和条目之间的线性近似相同,但所有这些都与IEEE浮点技巧相关。

编辑:根据特殊酱这是比CLR实施快3.25倍。 谢谢!

  1. 记住, 这个激活函数的任何改变都是以不同的行为为代价的 。 这甚至包括切换到浮动(从而降低精度)或使用激活替代品。 只有尝试一下你的用例才能显示正确的方法。
  2. 除了简单的代码优化之外,我还build议考虑计算的并行化 (即:利用您的机器的多个内核甚至是Windows Azure云中的机器)并改进训练algorithm。

更新: 发布neural network激活函数的查找表

UPDATE2:我删除了LUT的点,因为我已经把它们与完整的哈希混淆了。 感谢Henrik Gustafsson让我回到赛道上。 所以内存不是问题,虽然search空间仍然与本地极值​​混乱了一点。

在1亿次通话中,我开始怀疑分析器开销是否不会影响结果。 将计算replace为no-op并查看是否仍然报告占用60%的执行时间。

或者更好的是,创build一些testing数据,并使用秒表计时器来分析一百万个电话。

如果你能和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需要将内存alignment到边界。

使用SSE,我可以在大约半秒内计算出所有1亿个元素的结果。 但是,一次分配这么多的内存会花费你几乎三分之一的GB,所以我build议一次处理更多但是更小的arrays。 你甚至可以考虑使用一个100K或更多元素的双缓冲方法。

另外,如果元素的数量开始大量增加,您可能需要select在GPU上处理这些东西(只需创build一维float4纹理并运行非常平凡的片段着色器)。

FWIW,这是我已经发布的答案的C#基准。 (Empty是一个只返回0的函数,用于度量函数调用开销)

空function:79ms 0
原文:1576ms 0.7202294
简体:(女高音)681ms 0.7202294
近似值(Neil)441ms 0.7198783
位曼尼:(马提奴)836ms 0.72318
泰勒:(雷克斯·洛根)261ms 0.7202305
查询:(亨里克)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右上方的链接),但是我不知道它是否对您有很大的用处。净。

另外还有一点:为了快速培训大型networking,你使用的逻辑斯蒂玛(Sigmoid)非常糟糕。 LeCun等人在Efficient Backprop的 4.4节中使用了一些以零为中心的东西(实际上,读了整篇文章,这非常有用)。

在.NETmathalgorithm中,F#比C#具有更好的性能。 所以用F#重写neural network可能会提高整体性能。

如果我们在F#中重新实现LUT基准testing片段 (我已经使用稍微调整过的版本),那么结果代码如下:

  • 以588.8ms执行sigmoid1基准, 而不是3899.2ms
  • 在156.6ms中执行sigmoid2(LUT)基准testing, 而不是411.4 ms

更多细节可以在博客文章中find。 这是F#代码片段JIC:

 #light let Scale = 320.0f; let Resolution = 2047; let Min = -single(Resolution)/Scale; let Max = single(Resolution)/Scale; let range step ab = let count = int((ba)/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没有debugging器):

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

更新:基准更新使用10 ^ 7迭代使结果与C相媲美

UPDATE2:这里是从同一台机器的C实现的性能结果进行比较:

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

注意:这是本文的后续内容。

编辑:更新来计算这个和这个相同的东西,从这个灵感。

现在看看你让我做什么! 你让我安装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倍。 有人用Windows窗口获取调查内存使用情况和性能使用MS的东西:)

使用LUT进行激活function并不罕见,特别是在硬件中实现时。 如果您愿意包含这些types的表格,那么在这里有许多经过validation的概念变体。 但是,正如已经指出的那样,走样可能会成为一个问题,但也有办法。 进一步阅读:

  • NEURObjects by Giorgio Valentini (这里也有一篇论文)
  • 具有数字LUT激活function的neural network
  • 通过降低精度激活函数来提高neural network特征提取
  • 整数权值neural network与量化非线性激励函数的一种新的学习algorithm
  • 量化对高阶neural network的影响

一些陷阱:

  • 当你到达桌子外面时误差会上升(但在极端情况下收敛到0)。 为x约+ -7.0。 这是由于select的比例因子。 SCALE的较大值在中间范围中给出较高的误差,但在边缘处较小。
  • 这通常是一个非常愚蠢的testing,我不知道C#,这只是一个简单的转换我的C代码:)
  • Rinat Abdullin是非常正确的,别名和精确度损失可能会导致问题,但由于我没有看到variables,我只能build议你试试这个。 事实上,除了查找表的问题,我同意他所说的一切。

原谅复制粘贴编码…

 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()); } } 

首先想到的是:如何对值的一些统计variables?

  • “价值”的价值通常是小的-10 <=价值<= 10?

如果不是的话,你可以通过testing越界值来获得提升

 if(value < -10) return 0; if(value > 10) return 1; 
  • 价值观经常重复吗?

如果是这样,你也许可以从Memoization中获得一些好处(可能不会,但是检查….并不会伤害)

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

如果这两者都不能应用,那么正如其他人所build议的那样,也许你可以放弃降低乙状结肠的准确性。

女高音有一些很好的优化你的电话:

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

如果你尝试一个查找表,并发现它使用了太多的内存,你可以随时查看每个连续调用的参数值,并使用一些caching技术。

例如尝试caching最后的值和结果。 如果下一个调用与前一个调用具有相同的值,则不需要计算它,因为您已经caching了上一个结果。 如果当前的呼叫与以前的呼叫相同,即使是100次,也可以节省100万次计算。

或者,您可能会发现,在连续的10次调用中,value参数的平均值相同,因此您可以尝试caching最后10个值/答案。

想法:也许你可以用预先计算出的值来制作(大)查找表?

这有点偏离主题,但出于好奇,我做了与Java中的C , C#和F#相同的实现。 如果别人好奇,我会留在这里。

结果:

 $ 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优于Mono for OS X。在类似的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()); } } 

我意识到自从这个问题出现之后已经有一年了,但是由于讨论了与C#相关的F#和C性能,我碰到了这个问题。 我玩了一些来自其他响应者的样本,发现代表看起来执行的速度比常规方法调用要快,但是与F#相比,C#没有明显的性能优势 。

  • 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 

less一个乘法)。 这个函数具有奇对称性,其导数是微不足道的。 将它用于neural network需要将input之和除以总input数量(将域限制为[-1..1],这也是范围)。

女高音主题的温和变化:

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

既然你只是在一个单一的精度结果之后,为什么要Math.Exp函数计算一个双? 任何使用迭代求和的指数计算器(请参阅e x的扩展 )都需要更长的时间才能获得更高的精度。 而双是单身的两倍! 这样,你先转换成单个, 然后做你的指数。

但是expf函数应该会更快。 我没有看到需要女高音(浮动)投给传递给expf,除非C#不做隐式浮动双转换。

否则,只需使用真实的语言,如FORTRAN …

这里有很多很好的答案。 我会build议通过这种技术来运行它,只是为了确保

  • 你不会再多次打电话了。
    (有时函数被调用的不是必要的,只是因为它们很容易调用。)
  • 你不会用相同的参数反复地调用它
    (你可以使用memoization)

顺便说一句,你所拥有的function是反逻辑function,
或对数比率函数log(f/(1-f))的倒数。

(更新与性能测量)(再次更新与真实的结果:)

我认为查询表解决scheme在性能方面会让你感觉非常不快,内存和精度成本可以忽略不计。

下面的代码片段是C语言中的一个示例实现(我不会说c#能stream畅地干代码)。 它运行和执行得不错,但我敢肯定,它有一个错误:)

 #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空气):

 $ 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


去做:

There are things to improve and ways to remove weaknesses; how to do is is left as an exercise to the reader 🙂

  • Tune the range of the function to avoid the jump where the table starts and ends.
  • Add a slight noise function to hide the aliasing artifacts.
  • As Rex said, interpolation could get you quite a bit further precision-wise while being rather cheap performance-wise.

There are a much faster functions that do very similar things:

x / (1 + abs(x)) – fast replacement for TAHN

And similarly:

x / (2 + 2 * abs(x)) + 0.5 – fast replacement for SIGMOID

Compare plots with actual sigmoid

Doing a Google search, I found an alternative implementation of the Sigmoid function.

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

Is that correct for your needs? 它快吗?

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

1) Do you call this from only one place? If so, you may gain a small amount of performance by moving the code out of that function and just putting it right where you would normally have called the Sigmoid function. I don't like this idea in terms of code readability and organization but when you need to get every last performance gain, this might help because I think function calls require a push/pop of registers on the stack, which could be avoided if your code was all inline.

2) I have no idea if this might help but try making your function parameter a ref parameter. See if it's faster. I would have suggested making it const (which would have been an optimization if this were in c++) but c# doesn't support const parameters.

If you need a giant speed boost, you could probably look into parallelizing the function using the (ge)force. IOW, use DirectX to control the graphics card into doing it for you. I have no idea how to do this, but I've seen people use graphics cards for all kinds of calculations.

I've seen that a lot of people around here are trying to use approximation to make Sigmoid faster. However, it is important to know that Sigmoid can also be expressed using tanh, not only exp. Calculating Sigmoid this way is around 5 times faster than with exponential, and by using this method you are not approximating anything, thus the original behaviour of Sigmoid is kept as-is.

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

Of course, parellization would be the next step to performance improvement, but as far as the raw calculation is concerned, using Math.Tanh is faster than Math.Exp.