美文网首页
吹水牛顿迭代法

吹水牛顿迭代法

作者: pointertan | 来源:发表于2017-05-24 23:50 被阅读0次

    因为吹水的能力不佳,所以要先打个草稿,今天的吹水过程大概是:
    1、牛顿迭代法的演绎过程
    2、牛顿迭代法求n次方根
    3、牛顿迭代法求n次方根改进版
    4、牛逼哄哄的invsqrt求平方根倒数

    1、牛顿迭代法的演绎过程

    乍一听,好像很高大上,其实理解上没什么难度。
    牛顿迭代法就是在不断迭代的过程中逼近曲线的根,可以看做,它就是用来求曲线的根的。
    所以它是怎么个迭代的过程,先上个动图:

    牛顿迭代法的过程

    我用吹水的姿势描述一遍动图的过程,先是有一个曲线,然后要找到曲线和x轴的交点,这个时候,脑袋一拍,选x1作为第一点,在这个点上垂直于x轴的线与曲线的交点,在这个交点做与曲线的切线,而切线与x轴的交点就是下一个迭代的点,不断重复这个过程,直到找到曲线和x轴交点这个目标值。值得注意的是,这个方法是不断逼近,所以得到的值可能会与目标值存在误差,而误差小于一定范围就可以认为是目标值了。一个更加图文并茂更加通俗易懂的解释可以点击 这个这个这个这个

    2、牛顿迭代法求n次方根

    那为什么说牛顿迭代法能用来求n次根呢,其实在1中的目标值,即曲线与x轴相交点的x值,假设你的方程为 f(x) = x² - 64,当f(x) = 0时,x就是64的2次方根,以此类推,把2换成你想要求的n次方,就可以求出64的n次方根了。

    明白了演绎过程,那我们就来探索它的代数实现。在上代码前,先上个图


    牛顿迭代法某一点的求值图解

    每次迭代要求的点就是右绿色箭头指向的这个点,在图中可以看到黑色箭头标识的线段长度为-f(x),f'(x)是对f(x)的求导结果,也就是切线的斜率,所以绿色箭头线段的长度为-f(x)/f'(x),而左绿色箭头的点为x,所有右绿色箭头指向的值为x加上绿色线段的长度,即x-(f(x)/f'(x))。

    上面的代数实现完成后,就可以上代码了。嘻嘻嘻,最近在看Python版的SICP,所以贴上Python的代码,这篇文章也是因为看了这本书的1.6章节写的。def就是定义一个方法的意思。

    def newton_nth_root_of_a(n, a, x = 1, e = 1e-13):
        def f(x):
            return x ** n - a
        def df(x):
            return n * x ** (n - 1)
        
        gap = abs(f(x))
        while gap > e:
            x = x - f(x)/df(x)
            gap = abs(f(x))
        return x
        
    print(newton_nth_root_of_a(1, 64))  #64.0
    print(newton_nth_root_of_a(2, 64))  #8.0
    print(newton_nth_root_of_a(3, 64))  #4.0
    print(newton_nth_root_of_a(4, 64))  #2.82842712474619
    print(newton_nth_root_of_a(5, 64))  #2.29739670999407
    print(newton_nth_root_of_a(6, 64))  #2.0
    

    这里是在线可运行版本 ,不多不少,刚好十行,newton_nth_root_of_a方法定义中,n代表幂,a代表对a求根,x代表起始值,e代表误差范围,x和e已经设置了缺省值。f(x)是曲线,df(x)是f(x)的求导结果,在while循环中,x - f(x)/df(x)就是我们上面代数实现中的结果,直到f(x)的值小于误差,x就是最后的结果。

    3、牛顿迭代法求n次方根改进版

    明白了1、2其实整个过程就是这样了,那为啥有3,因为要硬着头皮接着吹水😂。
    2中的10行代码很简洁,但是假如我要不断的求某些值的2次根,就要不断调用newton_nth_root_of_a(2, a)。那么我们来改进下代码,保持上面的代码不变,增加下面的代码:

    def curry_nth_root(n):
        def curry_nth_root_of_a(a):
            return newton_nth_root_of_a(n, a)    
        return curry_nth_root_of_a
    
    _2th_root = curry_nth_root(2)
    
    print(_2th_root(64)) #8.0
    print(_2th_root(49)) #7.000000000000001
    print(_2th_root(36)) #6.0
    print(_2th_root(25)) #5.0
    print(_2th_root(16)) #4.0
    print(_2th_root(9))  #3.0
    

    这里是在线可运行版本 ,哇咔咔,在curry化之后,就可以方便的求n次方根了,不用每次都输入n。

    curry化其实也很简单,那么我们就来个极端点的抽象代码:

    def improve_guess(update, close, guess=1):
        '''返回猜想值,update为更新方法,close为逼近方法,guess为初始猜想值'''
        while not close(guess):
            guess = update(guess)
        return guess
        
    def newton_update(f, df):
        '''返回牛顿迭代的计算函数, f为原函数,df是对其求导'''
        def update(x):
            return x - f(x) / df(x)
        return update
        
    def newton_find_zero_of_f(f, df, tolerance=1e-13):
        '''找出f(x)=0时,x的值,tolerance为与目标真实值的误差'''
        def near_zero(x):
            return is_eq(f(x), 0, tolerance)
        return improve_guess(newton_update(f, df), near_zero)
        
    def is_eq(x, y, tolerance):
        '''x与y的差值是否在误差范围内'''
        return abs(x - y) < tolerance
        
    def nth_root_of_a(n, a):
        def f(x):
            return x ** n - a
        def df(x):
            return n * x ** (n - 1)
        return newton_find_zero_of_f(f, df)
        
    print(nth_root_of_a(1, 64))  #64.0
    print(nth_root_of_a(2, 64))  #8.0
    print(nth_root_of_a(3, 64))  #4.0
    print(nth_root_of_a(4, 64))  #2.82842712474619
    print(nth_root_of_a(5, 64))  #2.29739670999407
    print(nth_root_of_a(6, 64))  #2.0
    

    这里是在线可运行版本 ,把每一步都抽象出来,哈哈哈哈哈,这看上去很帅,虽然实际上没啥必要,最后也可以再curry化。这是个极端的例子,但是根据业务的需要,我们可以把需要复用的步骤抽象出来,而抽象的程度也要根据业务开展。

    4、牛逼哄哄的invsqrt求平方根倒数

    吹水时间又到了,究竟这个牛顿迭代法有个毛线用。那你就要google下invsqrt这个改变游戏史进程的函数,可以看下知乎上的这个回答 有哪些算法惊艳到了你?。这个函数到底是干嘛的,就是求某个数的2次方根的倒数。因为这个函数在3d引擎的中经常使用,所以提高这个函数的效率是至关重要的,这个函数比系统的1/sqrt(x)快了一半。虽然是倒数,但是也是能用牛顿迭代法的啊。为毛啊,因为这也是曲线啊,过程跟1中的图相识。

    但是!!!!请看代码

    float InvSqrt (float x){
        float xhalf = 0.5f*x;
        int i = *(int*)&x;
        i = 0x5f3759df - (i>>1);
        x = *(float*)&i;
        x = x*(1.5f - xhalf*x*x);
        return x;
    }
    

    第六行就是利用牛顿迭代法的值x - f(x)/f'(x)演算后的结果,在这里f(x) = 1/x²,则f'(x)=2/x³。
    第三行是把一个float指针强转成int指针,从而进行右移运算,因为float类型不能进行位运算,从而得到x值指数部分/2的结果,额,这里我不打算展开说啊,因为我说不清楚啊啊啊啊,我引用一段别人的解释,详情请移步这里

    所以,32位的浮点数用十进制实数表示就是:M2E。开根然后倒数就是:M(-1/2)2^(-E/2)。现在就十分清晰了。语句i> >1其工作就是将指数除以2,实现2(E/2)的部分。而前面用一个常数减去它,目的就是得到M(1/2)同时反转所有指数的符号。

    至于0x5f3759df,这就相当无解了。wiki的介绍中,这一句的注释是这样的:

        i  = 0x5f3759df - ( i >> 1 );               // what the fuck?(这他妈的是怎么回事?)
    

    关于这个值后来也有人专门去推理过,还写成论文,但是,我肯定是看不懂的!!!
    因为这一步,取到一个非常合适的初始值,只需要一次牛顿迭代就能知道目标值啦,一次!!!!

    最后

    没有最后,今天吹水结束!!!!!!!!

    相关文章

      网友评论

          本文标题:吹水牛顿迭代法

          本文链接:https://www.haomeiwen.com/subject/acgoxxtx.html