官术网_书友最值得收藏!

2.2 自動求導(dǎo)和人工智能框架

上一節(jié)的GD法解決了基于梯度的通用迭代算法問題,BP算法解決了多元復(fù)合函數(shù)求導(dǎo)問題,這使得我們向通用深度學(xué)習(xí)框架前進(jìn)了一大步。但是對于簡單函數(shù),我們?nèi)匀恍枰暶髅總€函數(shù)的導(dǎo)函數(shù),例如sin(x)的導(dǎo)函數(shù)是cos(x)等。接下來,我們從很容易理解的表達(dá)式出發(fā),一步一步地用面向?qū)ο蟮姆椒ń鉀Q這個問題。

2.2.1 表達(dá)式和自動求偏導(dǎo)

這一小節(jié)我們考慮的問題是:如何才能自動求得表達(dá)式的偏導(dǎo)函數(shù)?讓我們用面向?qū)ο蟮姆椒ㄟM(jìn)行思考。首先,把表達(dá)式抽象為一個名為Exp的類,根據(jù)需要,可以在其中定義方法deriv(),用來計算表達(dá)式針對某一變量的偏導(dǎo)函數(shù),見下面的代碼片段:

還記得我們是怎么考慮遞歸程序設(shè)計的嗎?第一步就是考慮遞歸邊界。同樣,在進(jìn)行面向?qū)ο笏伎紩r,第一步應(yīng)該考慮最簡單的表達(dá)式是什么。顯然,最簡單的表達(dá)式是常數(shù)和變量,例如3、5、xy等。所以我們在前面代碼的基礎(chǔ)上添加兩個類Const和Variable,它們都是Exp類的子類。代碼如下:

在Const的構(gòu)造函數(shù)__init__()中,我們定義了一個參數(shù)value來表示這個常數(shù)對象的值。在Variable的構(gòu)造函數(shù)中,我們定義了一個參數(shù)name來表示變量的名字,例如x。我們還重定義了Const和Variable的__repr__()方法。這樣,在打印一個表達(dá)式時,可以根據(jù)它是常數(shù)還是變量,調(diào)用不同的__repr__()方法,從而得到不同的字符串。

由于我們創(chuàng)建表達(dá)式Exp類的目的是求偏導(dǎo),所以接著要考慮的是常數(shù)和變量的偏導(dǎo)該怎么求?顯然,常數(shù)對任何自變量的偏導(dǎo)都是0。對于變量來說,如果它與自變量相同,偏導(dǎo)就是1;否則,偏導(dǎo)就是0。所以我們只需在Const和Variable中重定義deriv()函數(shù)即可。代碼如下:

比常數(shù)和變量稍復(fù)雜一點(diǎn)的表達(dá)式就是加減乘除。以加法為例,我們先定義Exp的子類Add。在Add的構(gòu)造函數(shù)__init__()中添加left和right兩個參數(shù),分別表示加法運(yùn)算的左右兩個子表達(dá)式。由于加法的偏導(dǎo)等于偏導(dǎo)的加法,所以很容易重定義deriv()。以下是代碼片段(我們把Add類置于類Variable之后,并對主程序做了輕微改動。代碼的其他部分與之前相同,保持不變):

輸出結(jié)果:

(123+x)

(0+1)

前面我們講分?jǐn)?shù)的加減乘除時(見代碼1-19),已經(jīng)知道如何把運(yùn)算符和類的內(nèi)部成員函數(shù)掛鉤,并且已經(jīng)知道了__add__()與__radd__()的區(qū)別。所以,通過重定義這些運(yùn)算符函數(shù)就可以達(dá)到簡化使用的目的。下面是對__add__()與__radd__()的重定義。我們在主程序中直接使用了+運(yùn)算符,并且,為了方便直接使用Python的常數(shù)(例如789),我們還新構(gòu)建了一個函數(shù)to_exp(),用來把一個可能的Python常數(shù)轉(zhuǎn)化為一個Exp對象。代碼的其他部分與之前相同,保持不變。

由于有了加法運(yùn)算符,所以這里就可以把Add.deriv()函數(shù)中的Add()調(diào)用換成+。

以此類推,我們可以把減、乘、除、乘方、求反、對數(shù)、正弦、余弦等幾乎所有的數(shù)學(xué)函數(shù)都實(shí)現(xiàn),進(jìn)而實(shí)現(xiàn)所有這些函數(shù)的任意組合運(yùn)算以及相應(yīng)的求導(dǎo)函數(shù)運(yùn)算。下面僅舉幾個簡單例子。

第一個例子是乘法。乘法求導(dǎo)的公式是(uv)′=u′v+uv′,不論uv是函數(shù)還是常數(shù),公式都成立。所以,它的求導(dǎo)實(shí)現(xiàn)如下,請在前面代碼的基礎(chǔ)上添加以下代碼:

注意,在重定義Mul的deriv()函數(shù)時,我們直接使用了?和+運(yùn)算符。

第二個例子是除法。對于除法運(yùn)算,讀者在重定義Exp的__rtruediv__()時,要注意左右子表達(dá)式的次序是顛倒的,不要出錯。這是因為Python語言對于形如A+B這樣的運(yùn)算是這樣考慮的:

1)如果A是一個對象,則調(diào)用這個對象的__add__(B)函數(shù),B是參數(shù)。如果這個函數(shù)不存在就報錯。

2)如果A是基本數(shù)據(jù)類型,例如整數(shù)(int)、浮點(diǎn)數(shù)(float)、布爾行(bool),則調(diào)用B的__radd__(A)。注意,add前多了一個r,并且A是參數(shù),所以B.__radd__(A)的真實(shí)含義和次序是A+B。

下面是除法的實(shí)現(xiàn):

第三個例子是乘方運(yùn)算。我們可以對yuv等號兩邊取對數(shù)然后再求導(dǎo),得到(uv)′=uv-1u′v+uv′lnu)。所以乘方及其偏導(dǎo)的實(shí)現(xiàn)是這樣的:

代碼2-11的最后一行我們調(diào)用了一個還沒有實(shí)現(xiàn)的函數(shù)log,這個函數(shù)用來實(shí)現(xiàn)求對數(shù)運(yùn)算。它的特殊地方在于,在Python中沒有與之對應(yīng)的運(yùn)算符,而加、減、乘、除、乘方等運(yùn)算是有對應(yīng)運(yùn)算符的,例如乘方的運(yùn)算符是兩個星號(??)。更一般地,在計算機(jī)語言中,大多沒有對數(shù)運(yùn)算符。即使有,一般也不允許用戶對其重定義。遇到這種情況,我們可以定義一個一般函數(shù)來代替,使用時同樣很方便。注意,(logvu)′=。請在類Const之后放置以下代碼,并且在文件頭部執(zhí)行import math,因為要定義常數(shù)e。

至此我們給出了在Python中有對應(yīng)運(yùn)算符和沒有對應(yīng)運(yùn)算符的函數(shù)實(shí)現(xiàn)的例子。以此類推,讀者請務(wù)必自行實(shí)現(xiàn)所有其他基本運(yùn)算和函數(shù),包括但不限于加、減、乘、除、乘方、對數(shù)、求反、絕對值、三角函數(shù)、反三角函數(shù)……如果某個函數(shù)或者運(yùn)算符沒有實(shí)現(xiàn),會影響后面程序的運(yùn)行。之后我們就可以輕松計算任意函數(shù)或者復(fù)合多元函數(shù)的偏導(dǎo)。下面是一個例子:

運(yùn)行結(jié)果是:

exp=(789+x)

deriv=(0+1)

exp=(x+789)

deriv=(1+0)

exp=((3 ?(x ??2))+(x ??5))

deriv =(((3 ?((x ??(2-1))?((1 ? 2)+((x ? 0)? log(x,2.718281828459045)))))+(0 ?(x ??2)))+((x ??(5-1))?((1 ?5)+((x ?0)?log(x,2.718281828459045)))))

其中最后一個結(jié)果是3x2+x5x的導(dǎo)數(shù)。結(jié)果是對的,但顯然太煩瑣了。優(yōu)化的辦法是對常數(shù)做特別處理,例如,3+5可以簡化為8,a+0可以簡化為a。我們可以在Exp類中定義一個simplify()函數(shù),返回一個簡化了的表達(dá)式,不能簡化時就返回self自身。下面,我們對加法和減法進(jìn)行簡化(省略號代表類或者代碼中的其他部分與上一節(jié)的代碼相同)。

請讀者按照同樣的方法簡化所有其他運(yùn)算和函數(shù)。注意,有些類型是不用簡化的,例如Const和Variable(想想看,為什么?)。

所有運(yùn)算和函數(shù)都被簡化之后,接下來我們就要考慮如何方便地使用simplify()函數(shù)。對于表達(dá)式Const(3)+Const(5)來說,我們期望能直接用Const(8)代替。為了達(dá)到這個目的,我們在Exp的所有運(yùn)算符函數(shù)(例如__add__())中對運(yùn)算結(jié)果調(diào)用simplify()即可。下面是在類Exp和函數(shù)log()中調(diào)用simplify()的示例[3]

最后運(yùn)行程序,得到的結(jié)果是:

exp=(789+x)

deriv=1

exp=(x+789)

deriv=1

exp=((3 ?(x ??2))+(x ??5))

deriv=((3 ?(x ?2))+((x ??4)?5))

2.2.2 表達(dá)式求值

當(dāng)自變量的值已經(jīng)給定時,我們有時希望給出表達(dá)式的值或?qū)?shù)的值。由于偏導(dǎo)函數(shù)也是一種表達(dá)式,所以,這個問題可歸結(jié)為如何求表達(dá)式的值。而Exp的所有子類中,只有變量Variable的值是不確定的,其他表達(dá)式的值可以通過計算得到。所以,問題又歸結(jié)為如何對變量進(jìn)行賦值。

根據(jù)面向?qū)ο蠓椒ǎ覀兘鉀Q這個問題的辦法是在父類Exp中定義一個新方法eval(??env[4])。該成員函數(shù)用來把當(dāng)前表達(dá)式轉(zhuǎn)為另一個表達(dá)式,參數(shù)evn中保存了變量的值。例如,當(dāng)x=5時,表達(dá)式x+3會被轉(zhuǎn)化為5+3,然后又簡化為8。代碼如下:

在上面的代碼中,我們先在Exp中定義了eval()函數(shù),然后在Exp的子類Neg、Const、Log、Variable中重定義了這個函數(shù)。請大家自行重定義Exp所有子類中的eval()函數(shù),然后運(yùn)行上面的程序(完整代碼見p02_6_eval.py),得到如下結(jié)果:

exp=((3 ?(x ??2))+(x ??5))

deric=((3 ?(x ?2))+((x ??4)?5))

exp[x=0]=0

deriv[x=0]=0

exp[x=1]=4

deriv[x=1]=11

exp[x=2]=44

deriv[x=2]=92

exp[x=3]=270

deriv[x=3]=423

exp[x=4]=1072

deriv[x=4]=1304

2.2.3 求解任意方程

前面我們介紹了GD法和表達(dá)式以及表達(dá)式求導(dǎo)和求值。下面把它們結(jié)合在一起,構(gòu)成一個智能框架,從而能夠求解用戶給出的任意方程或者方程組。

例如,為了求解方程x2+3x-10=0,我們首先要讓用戶輸入x??2+3?x-10這樣的字符串,然后通過Python的內(nèi)部函數(shù)eval()[5]把該字符串轉(zhuǎn)成Exp類型。最后,用GD法的式(2-2)求解該表達(dá)式等于0的解。根據(jù)自頂向下原則,我們先寫主程序:

主程序中的關(guān)鍵是用到了Python的內(nèi)部函數(shù)eval(),它可以把用戶輸入的字符串當(dāng)成一個Python表達(dá)式進(jìn)行求值。假設(shè)用戶輸入字符串x??2+3?x-10,eval()就會把它理解為Python表達(dá)式,然后計算它的值。你可以把字符串理解為Python代碼,替換eval()調(diào)用。例如exp=eval(exp)就相當(dāng)于exp=x??2 +3?x-10。由于我們在這一句之前定義了x=Variable(′x′),所以Python能夠調(diào)用我們前面為Exp編寫的代碼,從而構(gòu)成一個Exp表達(dá)式。假如沒有x=Variable(′x′)這一句,運(yùn)行時Python會報告變量x沒有定義。

接著我們調(diào)用了solve()函數(shù),它被用來求解表達(dá)式等于0的解。見代碼2-18:參數(shù)exp表示輸入的任意表達(dá)式;variable是該表達(dá)式中要求解的變量;epoches為int型,表示最大迭代次數(shù),缺省值為50000;lr為學(xué)習(xí)步長(learning rate),即式(2-2)中的a,缺省值為0.01;eps是解的精度要求,缺省值為0.001。我們按照算法2-1對函數(shù)solve()進(jìn)行了實(shí)現(xiàn)。

程序運(yùn)行時,用戶只需輸入一個合法的、以x為自變量的Python表達(dá)式即可。下面是個例子:

Please input the expression such as x??2+3?x-10,press enter to exit:x??2+3?x-10

(((x ??2)+(3 ? x))-10)

x=1.999921322820418

Please input the expression such as x??2+3?x-10,press enter to exit:

注意,如果希望用戶使用log()這樣的沒有運(yùn)算符的函數(shù),應(yīng)該在程序里把上節(jié)我們定義的log()函數(shù)引入(import)進(jìn)來;否則,程序無法識別這樣的函數(shù)。

2.2.4 求解任意方程組

對上節(jié)的代碼稍加改進(jìn),我們就可以求解任意多元方程組。方法是:

1)把solve的參數(shù)exp、variable分別改為exps和variables,表示輸入的方程和變量都是一個列表。

2)在solve的函數(shù)體中,把局部變量value改為values,對應(yīng)于每一個變量的值。

3)構(gòu)建一個新的表達(dá)式exp,它等于所有方程的平方和。這樣,在求得exp的最小值時,順帶就求得了原來方程組的解(下文我們會把這樣的exp稱為目標(biāo)函數(shù))。

4)計算并記錄exp對每一個變量的偏導(dǎo)函數(shù)derivs。

5)在循環(huán)體中求每一個變量的變化值,然后更新所有變量。

6)返回解即可。

解方程組的代碼如下:

所以,雖然上述代碼是針對方程組的,但它的邏輯與前面解方程的程序是等價的;只不過解方程時只處理一個未知數(shù)、一個值,而解方程組時處理一堆未知數(shù)、一堆值。算法邏輯并沒有改變。這也是一個比較重要的程序設(shè)計技巧,即先針對簡單的數(shù)編寫程序,然后再把其中的數(shù)據(jù)改為多維,但不改變程序邏輯,從而達(dá)到提升程序功能的目的。

這個程序允許用戶最多使用3個未知數(shù)xyz。輸入完畢后直接按回車鍵,系統(tǒng)就會自動對方程組進(jìn)行求解。下面是一個例子,求解方程組

Please input an expression with variables of x/y/z:3?x+2?y-6

(((3 ? x)+(2 ? y))-6)

Please input an expression with variables of x/y/z:x-5?y??2+1

((x-(5 ?(y ??2)))+1)

Please input an expression with variables of x/y/z:

x=1.5213930455427034

y=0.710675704190072

z=1.0

2.2.5 求解任意函數(shù)的極小值

前面我們給出了方程和方程組的解法。我們已經(jīng)構(gòu)建了一個通用程序,或者說程序設(shè)計框架的雛形。下面我們更進(jìn)一步,考慮求解任意多個函數(shù)的極小值。

在說明式(2-1)和式(2-2)時,已經(jīng)證明了解方程等價于求極小值,所以solve()函數(shù)只需做輕微改動即可。

運(yùn)行結(jié)果如下:

Please input an expression with variables of x/y/z:(x-2)??2

((x-2)??2)

Please input an expression with variables of x/y/z:(y+3)??2

((y+3)??2)

Please input an expression with variables of x/y/z:

x=1.987524486650139

y=-2.9500979466005552

z=1.0

min(((x-2)??2))=0.000155638433342562

min(((y+3)??2))=0.002490214933481036

2.2.6 張量、計算圖和人工智能框架

至此,我們已經(jīng)非常接近人工智能框架Tensorflow(簡稱為TF)的實(shí)質(zhì)了。TF的實(shí)質(zhì)是什么?TF的實(shí)質(zhì)就是一個自動求微分(偏導(dǎo)數(shù))的工具。TF中的基本概念其實(shí)就是表達(dá)式,即前面反復(fù)操作的Exp及其子類。只不過TF中不叫表達(dá)式,而是叫作張量。所謂張量,就是可以求值、求偏導(dǎo)的表達(dá)式對象。另外,TF中的張量不僅可以像Exp一樣取值為標(biāo)量,也可以取值為向量、矩陣或者高維矩陣;而我們的Exp只能取值為標(biāo)量[6]

張量可以構(gòu)成計算圖。那計算圖的實(shí)質(zhì)又是什么呢?計算圖的實(shí)質(zhì)就是依賴關(guān)系圖,即用來表明張量之間依賴關(guān)系的有向無環(huán)圖。由于后者與BP算法密不可分,所以BP算法自然也就成了TF的核心算法之一了。

張量、計算圖、GD法和BP算法構(gòu)成了TF的基礎(chǔ),TF的其他概念、算法幾乎都是建立在這個基礎(chǔ)之上的。現(xiàn)在,由于學(xué)習(xí)了表達(dá)式、求值和求偏導(dǎo),我們可以比較輕松地邁入TF的世界了。

主站蜘蛛池模板: 尖扎县| 沙雅县| 昌乐县| 西和县| 晋江市| 汝城县| 衡阳县| 徐州市| 旬阳县| 古蔺县| 宜州市| 武陟县| 曲阜市| 东至县| 尼勒克县| SHOW| 清水河县| 凤山市| 和龙市| 河间市| 犍为县| 阳城县| 武穴市| 清河县| 冷水江市| 舒兰市| 晋江市| 东兰县| 台北县| 景德镇市| 论坛| 保德县| 海丰县| 平舆县| 龙山县| 岳阳县| 酒泉市| 大宁县| 霍林郭勒市| 大竹县| 米易县|