牛頓迭代算法 sqrt函數

因爲吹水的能力不佳,所以要先打個草稿,今天的吹水過程大概是:
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?(這他媽的是怎麼回事?)

關於這個值後來也有人專門去推理過,還寫成論文,但是,我肯定是看不懂的!!!
因爲這一步,取到一個非常合適的初始值,只需要一次牛頓迭代就能知道目標值啦,一次!!!!

最後

沒有最後,今天吹水結束!!!!!!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章