【问题标题】:Math optimization in C#C# 中的数学优化
【发布时间】:2010-09-29 13:08:25
【问题描述】:

我整天都在分析一个应用程序,并且优化了一些代码,我的待办事项列表中只剩下了这个。它是神经网络的激活函数,被调用超过 1 亿次。根据 dotTrace 的数据,它约占整个函数时间的 60%。

你会如何优化它?

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

【问题讨论】:

  • 输入值的范围是多少?
  • 另外,输入值有多精确?有多少十进制数字对您很重要?
  • 越多越好,真的,但我会说大约 6-7 个十进制数字
  • 有没有简单的方法来确保方法是内联的?也许是 final 修饰符?
  • +1 用于在您确定需要优化之前进行分析!

标签: c# optimization neural-network performance


【解决方案1】:

请记住,Sigmoid 约束的结果范围在 0 到 1 之间。小于 -10 的值会返回一个非常非常接近 0.0 的值。大于 10 的值会返回一个非常非常接近 1 的值。

在过去,当计算机无法很好地处理算术上溢/下溢时,通常使用 if 条件来限制计算。如果我真的关心它的性能(或者基本上是 Math 的性能),我会将您的代码更改为老式的方式(并注意限制),这样它就不会不必要地调用 Math:

public double Sigmoid(double value)
{
    if (value < -45.0) return 0.0;
    if (value > 45.0) return 1.0;
    return 1.0 / (1.0 + Math.Exp(-value));
}

我意识到阅读此答案的任何人都可能参与某种 NN 开发。请注意上述情况如何影响您的训练分数的其他方面。

【讨论】:

    【解决方案2】:

    我看到这里很多人都在尝试使用近似来使 Sigmoid 更快。但是,重要的是要知道 Sigmoid 也可以使用 tanh 来表示,而不仅仅是 exp。 以这种方式计算 Sigmoid 比使用指数方式快约 5 倍,并且通过使用这种方法,您不会逼近任何东西,因此 Sigmoid 的原始行为保持原样。

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

    当然,并行化将是提高性能的下一步,但就原始计算而言,使用 Math.Tanh 比 Math.Exp 更快。

    【讨论】:

      【解决方案3】:

      看看this post。它有一个用 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 中)。该近似值基于论文“A Fast, Compact Approximation of the Exponential Function”,该论文是专门为用于神经网络而开发的。它与 2048 个条目的查找表和条目之间的线性近似基本相同,但所有这些都使用 IEEE 浮点技巧。

      编辑:根据Special Sauce,这比 CLR 实现快约 3.25 倍。谢谢!

      【讨论】:

      • 好奇:你能把 (1072693248 - 60801) 简化为 1072632447 吗?另外,你能把它放在长投之外,所以为了速度,它不会加到双倍上吗?这会影响准确性和/或性能吗?
      • (后见之明:我意识到减法可能已被编译器优化,但无论如何都值得一试。)
      • @strager 对,这肯定是由编译器优化的。我之所以这样留下它,是因为它部分是公式的开发方式,但您可以将其替换为 1072632447。
      • @martinus,您是否尝试过其他技术?改写为:long tmp = (long)(1512775 * val) + 1072632447;
      • 我在 C# 中对这个非常简单的函数进行了基准测试,发现它比 CLR 实现快 ~3.25 倍。为了了解误差水平,这里有五个不同数量级的随机示例对(CLR 结果,近似结果):(0.0007242, 0.0007376), (1.55306, 1.57713), (307.78015, 309.18896), (1093286.54660, 1050935.0), (9.76825E+30, 9.57295E+30)
      【解决方案4】:

      有一些更快的函数可以做非常相似的事情:

      x / (1 + abs(x)) – 快速替换 TAHN

      同样:

      x / (2 + 2 * abs(x)) + 0.5 - 快速替换 SIGMOID

      Compare plots with actual sigmoid

      【讨论】:

        【解决方案5】:

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

        我认为查找表解决方案可以让您在性能方面走得更远,而内存和精度成本可以忽略不计。

        下面的 sn-p 是 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
        


        待办事项:

        有需要改进的地方和消除弱点的方法;怎么做留给读者作为练习:)

        • 调整函数的范围,避免表格开始和结束的跳转。
        • 添加轻微噪声功能以隐藏锯齿伪影。
        • 正如 Rex 所说,插值可以让您在精度方面更进一步,同时在性能方面相当便宜。

        【讨论】:

        • 你正在做的是定点数学。我更喜欢使用 2^n 的比例因子,例如 256、1024、65536。理想的比例因子是 2^8 或 2^16,然后您可以抓取字节来获取整数部分。如果在整个应用程序中使用固定点的表格,您可能会获得更大的提升。
        • 好吧,我故意不使用定点数学。我当然可以,但我宁愿避免这种额外的别名来源。
        • 此外,我没有使用任何更神奇的位旋转优化,因为代码应该是 1. 可读,2. 易于转换为 C#(其中大部分无论如何都是无用的) .
        • 当然,在任何地方使用 32 位整数可能是最快的,但考虑到 sigmoid 函数的性质,定点数学可能不是最佳选择。 (我不喜欢评论限制:)
        • 我使用的另一种方法是生成一个表格,该表格是一个浮点数组,然后在点之间进行插值。摆脱量化效应。根据您构建它的方式,它仍然可以很快。对于像这样缩小范围的函数可能会很好。
        【解决方案6】:

        注意:这是this帖子的后续内容。

        编辑:更新以计算与thisthis 相同的东西,从this 获得一些灵感。

        现在看看你让我做什么!你让我安装 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 用于激活功能并不少见,尤其是在硬件中实现时。如果您愿意包含这些类型的表格,那么有许多经过充分验证的概念变体。然而,正如已经指出的那样,混叠可能会成为一个问题,但也有解决这个问题的方法。进一步阅读:

        一些问题:

        • 当您到达桌子外时,误差会上升(但在极端情况下会收敛到 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());
            }
        }
        

        【讨论】:

        • 感谢您的更新。请注意,您可能想要选择不同的误差测量值,否则会更改 -6.0f; x
        • 另外在发布模式下编译代码并在不附加调试器的情况下运行它会产生结果:1195ms vs 41ms。这快了 10 倍以上))
        • 但是修复 Sigmoid1 将速度优势降低到 10 倍。此外,可以通过保存中间值将 Sigmoid2 提高 2ms。见:rabdullin.com/journal/2009/1/5/…
        • 我没有费心在那里变得聪明。我认为最好保持一个真正的简单并接近en.wikipedia.org/wiki/Approximation_error 处理好 v1=0 需要完全不同的东西,但这是该测量的固有、众所周知和理解的弱点。
        • 关于“局部极值”,我们用来在项目中消除其影响的一种方法(与机器学习完全无关)是在信号中添加一些噪声。我认为添加这种不确定性有助于学习算法不会卡在那里。
        【解决方案7】:

        我意识到这个问题出现已经一年了,但由于讨论了 F# 和 C 相对于 C# 的性能,我遇到了这个问题。我使用了来自其他响应者的一些示例,发现委托似乎比常规方法调用执行得更快,但 there is no apparent peformance advantage to F# over C#

        • C: 166 毫秒
        • C#(委托):275ms
        • C#(方法):431ms
        • C#(方法、浮点计数器):2,656 毫秒
        • F#:404 毫秒

        带有浮点计数器的 C# 是 C 代码的直接移植。在 for 循环中使用 int 会快得多。

        【讨论】:

          【解决方案8】:

          这里有很多很好的答案。我建议通过this technique 运行它,以确保

          • 您无需多次调用它。
            (有时函数被调用的次数过多,只是因为它们很容易调用。)
          • 您不会使用相同的参数重复调用它
            (你可以使用记忆)

          顺便说一句,您拥有的函数是逆 logit 函数,
          或 log-odds-ratio 函数的倒数 log(f/(1-f))

          【讨论】:

            【解决方案9】:

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

            例如,如果您使用近似值 (1+x/256)^256,在我用 Java 进行的 Pentium 测试中(我假设 C# 本质上编译为相同的处理器指令),这大约是 7-8 倍e^x (Math.exp()),并且精确到小数点后 2 位,直到大约 +/-1.5 的 x,并且在您声明的范围内的正确数量级内。 (显然,要提高到 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 加倍或减半(以及添加/删除乘法)。即使 n=4,它仍然可以为 -0.5 和 0.5 之间的 x 值提供大约 1.5 个小数位的精度(并且看起来比 Math.exp() 快 15 倍)。

            附:我忘了提——你显然不应该真的除以 256:乘以常数 1/256。 Java 的 JIT 编译器会自动进行这种优化(至少 Hotspot 会),我假设 C# 也必须这样做。

            【讨论】:

            • 哇。这进一步降低了它!
            • 如果你要乘以或除以 2 的幂,左移或右移(>),而不是使用乘法/除法,它会快得多。
            • @nicodemus13 - 这适用于整数情况,尽管在现代处理器上不一定比乘法更快。但是你真的不如让编译器执行那种优化。
            • 但不要假设您 20 年前的处理器时序和优化概念仍然适用。您可能会发现您的处理器可以在整数移位的同时进行 FP 乘法...
            【解决方案10】:

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

            空函数:79ms 0 原文:1576ms 0.7202294 简化:(女高音)681ms 0.7202294 近似值:(尼尔)441ms 0.7198783 位操作:(martinus)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));
            }
            

            【讨论】:

            【解决方案11】:

            F# 在 .NET 数学算法中的性能优于 C#。因此在 F# 中重写神经网络可能会提高整体性能。

            如果我们在 F# 中重新实现 LUT benchmarking snippet(我一直在使用稍微调整过的版本),那么生成的代码:

            • 588.8ms 而不是 3899.2ms 内执行 sigmoid1 基准测试
            • 156.6 毫秒而不是 411.4 毫秒内执行 sigmoid2 (LUT) 基准测试

            更多详情请见blog post。这是 F# sn-p 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 相当

            UPDATE2:这里是来自同一台机器的C implementation 的性能结果,用于比较:

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

            【讨论】:

            • 我会用计时更新 C 代码,但我不能更新到 F#,而且我们的机器差异很大,我认为如果你运行测试会更好。敬请期待
            • 嘿,你正在做 10^7 次迭代,不是吗?
            • 呃,是的。我已经在博客文章中修正了这个错字并忘记了这个 sn-p。谢谢。 Re C sn-p,稍后我会在我的机器上运行它。只需要获得一些 C 编译器。
            • 这些是我的单声道数字:10^7 次迭代使用 sigmoid1:1661.244000 毫秒 10^7 次迭代使用 sigmoid2:732.762000 毫秒
            • @Rinat Abdullin:您的基准测试是错误的。您观察到的效果是在 C# 的 for 循环中使用 float 作为计数器。如果像在 F# 中那样在 C# 中使用 int 作为计数器和委托来执行 sigmoid 算法,则 C# 会稍微快一些。 thoughtfulcode.wordpress.com/2010/12/30/…
            【解决方案12】:

            这有点离题,但出于好奇,我做了与 Java 中 CC#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 在 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());
                }
            }
            

            【讨论】:

            • 只希望它完整:)
            【解决方案13】:
            1. 请记住,此激活函数的任何更改都会以不同的行为为代价。这甚至包括切换到浮点数(从而降低精度)或使用激活替代品。只有对您的用例进行试验才能找到正确的方法。
            2. 除了简单的代码优化之外,我还建议考虑计算并行化(即:利用您机器的多个内核,甚至是 Windows Azure 云中的机器)并改进训练算法。

            更新: Post on lookup tables for ANN activation functions

            UPDATE2:我删除了 LUT 上的点,因为我将它们与完整的散列混淆了。感谢Henrik Gustafsson 让我重回正轨。所以内存不是问题,尽管搜索空间仍然会被局部极值弄乱。

            【讨论】:

            • 我已经在并行化部分代码,有些使用 Parallel.For,有些使用 PLINQ。到目前为止,它的工作很棒
            • 是的,我会怀疑。但是,当训练算法(我使用允许选择网络结构的进化算法)在多台机器上运行时,整个乐趣就开始了))
            • 对双精度范围的每个可能值使用 LUT 会占用您所有的内存,但如果使用浮点数并且可以接受一些精度损失,则表可能适合小于 4k 的内存.你的陈述 1 是错误的。
            • 如果可以接受激活函数中约 0.1% 的错误,我在下面发布的 C 示例如果你稍微修复它就可以工作(TODO 中的前两点应该这样做: )
            • 我试试看。我也在尝试最小化内存使用(与您的帖子无关) - 显然 .NET 认为传递一个巨大的数组是一个好主意......等等......
            【解决方案14】:

            如果您需要大幅提升速度,您可能会考虑使用 (ge)force 并行化函数。 IOW,使用 DirectX 来控制显卡为你做这件事。我不知道该怎么做,但我见过人们使用显卡进行各种计算。

            【讨论】:

              【解决方案15】:

              如果您能够与 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 亿个元素的结果。但是,一次分配这么多内存将花费您近三分之二的千兆字节,因此我建议一次处理更多但更小的数组。您甚至可能需要考虑使用具有 100K 或更多元素的双缓冲方法。

              此外,如果元素的数量开始显着增加,您可能需要选择在 GPU 上处理这些事情(只需创建一个 1D float4 纹理并运行一个非常简单的片段着色器)。

              【讨论】:

              • +1 用于在正常范围之外思考并使用硬件加速。
              【解决方案16】:

              Soprano 主题的轻微变化:

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

              既然您只追求单精度结果,为什么要让 Math.Exp 函数计算双精度?任何使用迭代求和的指数计算器(请参阅the expansion of the ex)每次都需要更长的时间才能获得更高的精度。双倍是单倍的工作量!这样,您首先转换为单一,然后做您的指数。

              但是 expf 函数应该更快。不过,除非 C# 不进行隐式浮点双精度转换,否则我认为不需要将女高音的 (float) 强制转换为 expf。

              否则,只需使用真正的语言,例如 FORTRAN...

              【讨论】:

              • Math.Exp 什么时候使用浮点数?
              【解决方案17】:

              在我的脑海中,this paper explains a way of approximating the exponential by abusing floating point,(单击右上角的链接以获取 PDF)但我不知道它在 .NET 中是否对您有很大用处。

              另外,还有一点:为了快速训练大型网络,您使用的逻辑 sigmoid 非常糟糕。请参阅Efficient Backprop by LeCun et al 的第 4.4 节并使用以零为中心的内容(实际上,请阅读整篇论文,它非常有用)。

              【讨论】:

              • 您的论文链接现在似乎已损坏。
              【解决方案18】:

              您也可以考虑尝试评估成本更低的替代激活函数。例如:

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

              (可以分解为

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

              减一乘)。该函数具有奇对称性,其导数微不足道。将其用于神经网络需要通过除以输入总数来归一化输入总和(将域限制为 [-1..1],这也是范围)。

              【讨论】:

                【解决方案19】:

                1) 你只从一个地方调用这个吗?如果是这样,您可以通过将代码移出该函数并将其放在您通常调用 Sigmoid 函数的正确位置来获得少量性能。在代码可读性和组织方面我不喜欢这个想法,但是当你需要获得最后的性能提升时,这可能会有所帮助,因为我认为函数调用需要在堆栈上推送/弹出寄存器,如果你的代码都是内联的。

                2) 我不知道这是否会有所帮助,但请尝试将您的函数参数设置为 ref 参数。看看是不是更快。我会建议将其设为 const(如果这是在 c++ 中,这将是一种优化),但 c# 不支持 const 参数。

                【讨论】:

                  【解决方案20】:

                  Soprano 对您的通话进行了一些不错的优化:

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

                  如果您尝试查找表并发现它使用了太多内存,您可以始终查看每次连续调用的参数值并采用一些缓存技术。

                  例如尝试缓存最后一个值和结果。如果下一个调用与前一个调用具有相同的值,则不需要计算它,因为您已经缓存了最后一个结果。如果当前调用与上一次调用相同,即使 100 次中有 1 次,您可能会为自己节省 100 万次计算。

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

                  【讨论】:

                    【解决方案21】:

                    首先想到:values 变量的一些统计数据怎么样?

                    • “value”的值是否通常很小 -10

                    如果没有,您可能可以通过测试超出范围的值来获得提升

                    if(value < -10)  return 0;
                    if(value > 10)  return 1;
                    
                    • 这些值是否经常重复?

                    如果是这样,您可能会从Memoization 中获得一些好处(可能不会,但检查一下也无妨......)

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

                    如果这些都不能应用,那么正如其他一些人所建议的那样,也许你可以降低 sigmoid 的准确性......

                    【讨论】:

                    • 这种情况下的记忆可能相当昂贵。
                    • Henrik:很可能,是的。根据将相同值传递给函数的频率,它可能仍然值得。我不确定算法的其余部分如何使用此函数,但它可能会不必要地多次调用它。
                    • 我们正在处理浮点数和神经网络,我认为这些值将无处不在 :)
                    【解决方案22】:

                    试试:

                    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 &lt;math.h&gt;,您应该可以使用它,它使用 float-exp功能。可能会快一点。

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

                    此外,如果您要进行数百万次调用,函数调用开销可能是个问题。尝试制作一个内联函数,看看是否有帮助。

                    【讨论】:

                    • 你知道值参数的范围吗?如果是这样,请考虑生成一个查找表。
                    • 你改变了值的符号,我的数学生锈了,但我不认为这是一回事......你应该有 Math.Exp(-value) 根据初始代码。跨度>
                    • @Marcel:不,他将 e^-value 更改为 1/(e^value),然后加上 1.0 并交换了分子/分母。
                    • 请见谅,为什么要转换成浮点数?浮点数不是从双精度数派生的吗?似乎如果是这种情况,使用双倍会更好?
                    • 那么是1 / (1+k) 还是k / (1+k)
                    【解决方案23】:

                    通过 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

                    【讨论】:

                    • 试一下atm,我会在一分钟内回复
                    【解决方案24】:

                    在 1 亿次调用中,我开始怀疑分析器开销是否会影响您的结果。将计算替换为no-op,看看是否仍然报告消耗60%的执行时间...

                    或者更好的是,创建一些测试数据并使用秒表计时器来分析大约一百万次调用。

                    【讨论】:

                      【解决方案25】:

                      想法:也许您可以使用预先计算的值制作一个(大)查找表?

                      【讨论】:

                      • 我会试试看的。希望这张桌子不会增长到巨大的比例。
                      • hb:这可能会适得其反。如果您不确定最大大小,则必须实现一个大小有限的结构(有点像缓存),这不是一项简单的任务。
                      • 我知道 - 给它一个快速测试,得到 OutOfMemoryException。女高音的功能帮助了很多
                      • 查找时间会比 Math.Exp 时间快吗?
                      • 也许表格结合插值,=使用定点数学(即缩放整数)会起作用。在过去,88000 DSP 处理器支持机器码。
                      猜你喜欢
                      • 2015-02-04
                      • 1970-01-01
                      • 2019-04-11
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多