原文:http://blog.csdn.net/ecitnet/article/details/1799444html
遊戲編程中的人工智能技術
.
>
.
(連載之一)
用日常語言介紹神經網絡
(Neural Networks in Plain English)
由於咱們沒有很好了解大腦,咱們常常試圖用最新的技術做爲一種模型來解釋它。在我童年的時候,咱們都堅信大腦是一部電話交換機。(否 則它還能是什麼呢?)我當時還看到英國著名神經學家謝林頓把大腦的工做挺有趣地比做一部電報機。更早些時候,弗羅伊德常常把大腦比做一部水力發電機,而萊 布尼茨則把它比做了一臺磨粉機。我還聽人說,古希臘人把大腦功能想象爲一付彈弓。顯然,目前要來比喻大腦的話,那隻多是一臺數字電子計算機了。 -
John R.Searle[注1]
|
神經網絡介紹(Introduction to Neural Networks
)
曾有很長一個時期,人工神經網絡對我來講是徹底神祕的東西。固然,有關它們我在文獻中已經讀過了,我也能描述它們的結構和工做機理,但我始終沒有能「啊 哈!」一聲,如同你頭腦中一個難於理解的概念有幸忽然獲得理解時的感受那樣。個人頭上好象一直有個榔頭在敲着,或者像電影Animal House(中文片名爲「動物屋」)中那個在痛苦地尖叫「先生,謝謝您,再給我一個啊!」的可憐傢伙那樣。我沒法把數學概念轉換成實際的應用。有時我甚至 想把我讀過的全部神經網絡的書的做者都抓起來,把他們縛到一棵樹上,大聲地向他們吼叫:「不要再給我數學了,快給我一點實際東西吧!」。但無需說,這是永遠不可能發生的事情。我不得不本身來填補這個空隙...由此我作了在那種條件下惟一能夠作的事情。我開始幹起來了。<一笑>
這 樣幾個星期後,在一個美麗的日子裏,當時我在蘇格蘭海邊度假,當我越過一層薄霧凝視着狹長的海灣時,個人頭腦忽然受到一個衝擊。一會兒悟到了人工神經網絡 是怎樣工做的。我獲得「啊哈!」的感受了!但我此時身邊只有一個賬篷和一個睡袋,還有半盒子的脆玉米片,沒有電腦可讓我迅速寫出一些代碼來證明個人直 覺。Arghhhhh!這時我纔想到我應該買一臺手提電腦。無論怎樣,幾天後我回到家了,我馬上讓個人手指在鍵盤上飛舞起來。幾個小時後個人第一人工神經 網絡程序終於編成和運行了,而且工做得挺好!天然,代碼寫的有點亂,須要進行整理,但它確實已能工做了,而且,更重要的是,我還知道它爲何能工做!我可 以告訴你,那天我是一位很是得意的人。
我但願本書傳遞給你的就是這種「啊哈!」感受。當咱們學完遺傳算法時,你可能已嚐到了一點感受,但你但願這種感受是美妙的話,那就要等把神經網絡部分整個學完。
生物學的神經網絡-大腦
(A Biological Neural Network–The Brain)
.... 你的大腦是一塊灰色的、像奶凍同樣的東西。它並不像電腦中的CPU那樣,利用單個的處理單元來進行工做。若是你有一具新鮮地保存到福爾馬林中的屍體,用一 把鋸子當心地將它的頭骨鋸開,搬掉頭蓋骨後,你就能看到熟悉的腦組織皺紋。大腦的外層象一個大核桃那樣,所有都是起皺的
[圖0左],這一層組織就稱皮層(Cortex)。若是你再當心地用手指把整個大腦從頭顱中端出來,再去拿一把外科醫生用的手術刀,將大腦切成片,那麼你將看到大腦有兩層
[圖0右]: 灰色的外層(這就是「灰質」一詞的來源,但沒有通過福爾馬林固定的新鮮大腦實際是粉紅色的。) 和白色的內層。灰色層只有幾毫米厚,其中緊密地壓縮着幾十億個被稱做neuron(神經細胞、神經元)的微小細胞。白色層在皮層灰質的下面,佔據了皮層的 大部分空間,是由神經細胞相互之間的無數鏈接組成。皮層象核桃同樣起皺,這能夠把一個很大的表面區域塞進到一個較小的空間裏。這與光滑的皮層相比能容納更 多的神經細胞。人的大腦大約含有1OG(即100億)個這樣的微小處理單元;一隻螞蟻的大腦大約也有250,OOO個。
如下
表l顯示了人和幾種動物的神經細胞的數目。
 |
 |
圖0-1 大腦半球像核桃 |
圖0-2 大腦皮層由灰質和白質組成 |
圖0 大腦的外形和切片形狀git
表l 人和幾種動物的神經細胞的數目
動 物
|
神經細胞的數目(數量級)
|
蝸 牛
|
10,000 (=10^4)
|
蜜 蜂
|
100,000 (=10^5)
|
蜂 雀
|
10,000,000 (=10^7)
|
老 鼠
|
100,000,000 (=10^8)
|
人 類
|
10,000,000,000 (=10^10)
|
大 象
|
100,000,000,000 (=10^11)
|
|
圖1神經細胞的結構
|
在人的生命的最初9個月內,這些細胞以每分鐘25,000個的驚人速度被建立出來。神經細胞和人身上任何其餘類型細胞十分不一樣,每一個神經細胞都長着一根 像電線同樣的稱爲軸突(axon)的東西,它的長度有時伸展到幾釐米[譯註],用來將信號傳遞給其餘的神經細胞。神經細胞的結構如
圖1所 示。它由一個細胞體(soma)、一些樹突(dendrite) 、和一根能夠很長的軸突組成。神經細胞體是一顆星狀球形物,裏面有一個核(nucleus)。樹突由細胞體向各個方向長出,自己可有分支,是用來接收信號 的。軸突也有許多的分支。軸突經過分支的末梢(terminal)和其餘神經細胞的樹突相接觸,造成所謂的突觸(Synapse,圖中未畫出),一個神經 細胞經過軸突和突觸把產生的信號送到其餘的神經細胞。
每一個神經細胞經過它的樹突和大約10,000個其餘的神經細胞相連。這就使得你的頭腦中全部神經細胞之間鏈接總計可能有l,000,000,000,000,000個。這比100兆個現代電話交換機的連線數目還多。因此絕不奇怪爲何咱們有時會產生頭疼毛病!
有趣的事實 曾經有人估算過,若是將一我的的大腦中全部神經細胞的軸突和樹突依次鏈接起來,並拉成一根直線,可從地球連到月亮,再從月亮返回地球。若是把地球上全部人腦的軸突和樹突鏈接起來,則能夠伸展到離開們最近的星系!
|
神 經細胞利用電-化學過程交換信號。輸入信號來自另外一些神經細胞。這些神經細胞的軸突末梢(也就是終端)和本神經細胞的樹突相遇造成突觸 (synapse),信號就從樹突上的突觸進入本細胞。信號在大腦中實際怎樣傳輸是一個至關複雜的過程,但就咱們而言,重要的是把它當作和現代的計算機一 樣,利用一系列的0和1來進行操做。就是說,大腦的神經細胞也只有兩種狀態:興奮(fire)和不興奮(即抑制)。發射信號的強度不變,變化的僅僅是頻 率。神經細胞利用一種咱們還不知道的方法,把全部從樹突上突觸進來的信號進行相加,若是所有信號的總和超過某個閥值,就會激發神經細胞進入興奮 (fire)狀態,這時就會有一個電信號經過軸突發送出去給其餘神經細胞。若是信號總和沒有達到閥值,神經細胞就不會興奮起來。這樣的解釋有點過度簡單 化,但已能知足咱們的目的。
神經細胞利用電-化學過程交換信號。輸入信號來自另外一些神經細胞。這些神經細胞的軸突末梢(也就是終端) 和本神經細胞的樹突相遇造成突觸(synapse),信號就從樹突上的突觸進入本細胞。信號在大腦中實際怎樣傳輸是一個至關複雜的過程,但就咱們而言,重 要的是把它當作和現代的計算機同樣,利用一系列的0和1來進行操做。就是說,大腦的神經細胞也只有兩種狀態:興奮(fire)和不興奮(即抑制)。發射信 號的強度不變,變化的僅僅是頻率。神經細胞利用一種咱們還不知道的方法,把全部從樹突上突觸進來的信號進行相加,若是所有信號的總和超過某個閥值,就會激 發神經細胞進入興奮(fire)狀態,這時就會有一個電信號經過軸突發送出去給其餘神經細胞。若是信號總和沒有達到閥值,神經細胞就不會興奮起來。這樣的 解釋有點過度簡單化,但已能知足咱們的目的。
正是因爲數量巨大的鏈接,使得大腦具有難以置信的能力。儘管每個神經細胞僅僅工做於大約100Hz的頻率,但因各個神經細胞都以獨立處理單元的形式並行工做着,令人類的大腦具備下面這些很是明顯的特色:
能實現無監督的學習。 有 關咱們的大腦的難以置信的事實之一,就是它們可以本身進行學習,而不須要導師的監督教導。若是一個神經細胞在一段時間內受到高頻率的刺激,則它和輸入信號 的神經細胞之間的鏈接強度就會按某種過程改變,使得該神經細胞下一次受到激勵時更容易興奮。這一機制是50多年之前由Donard Hebb在他寫的Organination of Behavior一書中闡述的。他寫道:
「當神經細胞 A的一個軸突重複地或持久地激勵另外一個神經細胞B後,則其中的一個或同時兩個神經細胞就會發生一種生長過程或新陳代謝式的變化,使得勵 B細胞之一的A細胞,它的效能會增長」
|
與此相反的就是,若是一個神經細胞在一段時間內不受到激勵,那麼它的鏈接的有效性就會慢慢地衰減。這一現象就稱可塑性(plasticity)。
對損傷有冗餘性(tolerance)。大 腦即便有很大一部分受到了損傷,它仍然可以執行復雜的工做。一個著名的試驗就是訓練老鼠在一個迷宮中行走。而後,科學家們將其大腦一部分一部分地、愈來愈 大地加以切除。他們發現,即便老鼠的很大的一部大腦被切除後,它們仍然能在迷宮中找到行走路徑。這一事實證實了,在大腦中,知識並非保存在一個局部地 方。另外所做的一些試驗則代表,若是大腦的一小部分受到損傷,則神經細胞能把損傷的鏈接從新生長出來。
處理信息的效率極高。神 經細胞之間電-化學信號的傳遞,與一臺數字計算機中CPU的數據傳輸相比,速度是很是慢的,但因神經細胞採用了並行的工做方式,使得大腦可以同時處理大量 的數據。例如,大腦視覺皮層在處理經過咱們的視網膜輸入的一幅圖象信號時,大約只要100ms的時間就能完成。考慮到你的神經細胞的平均工做頻率只有 100Hz,100ms的時間就意味只能完成10個計算步驟!想想經過咱們眼睛的數據量有多大,你就能夠看到這真是一個難以置信的偉大工程了。
善於概括推廣。大腦和數字計算機不一樣,它極擅長的事情之一就是模式識別,並能根據已熟悉信息進行概括推廣(generlize)。例如,咱們可以閱讀他人所寫的手稿上的文字,即便咱們之前歷來沒見過他所寫的東西。
它是有意識的。意 識(consciousness)是神經學家和人工智能的研究者普遍而又熱烈地在辯論的一個話題。有關這一論題已有大量的文獻出版了,但對於意識實際究竟 是什麼,至今還沒有取得實質性的統一見解。咱們甚至不能贊成只有人類纔有意識,或者包括動物王國中人類的近親在內纔有意識。一頭猩猩有意識嗎?你的貓有意識 嗎?上星期晚餐中被你吃掉的那條魚有意識嗎?
所以,一我的工神經網絡(Artificial neural network,簡稱ANN)就是要在當代數字計算機現有規模的約束下,來模擬這種大量的並行性,並在實現這一工做時,使它能顯示許多和生物學大腦相相似的特性。下面就讓咱們瞧瞧它們的表演吧!
遊戲編程中的人工智能技術
.
3 數字版的神經網絡 (The Digital Version)
上面咱們看到了生物的大腦是由許多神經細胞組成,一樣,模擬大腦的人工神經網絡ANN是由許多叫作人工神經細胞(Artificial neuron,也稱人工神經原,或人工神經元)的細小結構模塊組成。人工神經細胞就像真實神經細胞的一個簡化版,但採用了電子方式來模擬實現。一我的工神 經網絡中須要使用多少個數的人工神經細胞,差異能夠很是大。有的神經網絡只須要使用10個之內的人工神經細胞,而有的神經網絡可能須要使用幾千我的工神經 細胞。這徹底取決於這些人工神經網絡準備實際用來作什麼。
有趣的事實 有 一個叫 Hugo de Garis的同行,曾在一個雄心勃勃的工程中建立並訓練了一個包含1000,000,000我的工神經細胞的網絡。這我的工神經網絡被他很是巧妙地創建起 來了,它採用蜂房式自動機結構,目的就是爲一機器客戶定製一個叫作CAM BrainMachine(「CAM大腦機器」) 的機器(CAM就是Cellular Automata Machine的縮寫)。此人曾自詡地宣稱這一人工網絡機器將會有一隻貓的智能。許多神經網絡研究人員認爲他是在「登星」了,但不幸的是,僱用他的公司在 他的夢想還沒有實現以前就破產了。此人如今猶他州,是猶他州大腦工程(Utah Brain Project)的領導。時間將會告訴咱們他的思想最終是否能變成實際有意義的東西。[譯註] |
我想你如今可能很想知道,一我的工神經細胞到底是一個什麼樣的東西?可是,它實際上什麼東西也不像; 它只是一種抽象。仍是讓咱們來察看一下圖2吧,這是表示一我的工神經細胞的一種形式。算法
圖2 一我的工神經細胞
圖中,左邊幾個灰底圓中所標字母w表明浮點數,稱爲權重(weight,或權值,權數)。進入人工神經細胞的每個input(輸入)都與一個權重w相 聯繫,正是這些權重將決定神經網絡的總體活躍性。你如今暫時能夠設想全部這些權重都被設置到了-1和1之間的一個隨機小數。由於權重可正可負,故能對與它 關聯的輸入施加不一樣的影響,若是權重爲正,就會有激發(excitory)做用,權重爲負,則會有抑制(inhibitory)做用。當輸入信號進入神經 細胞時,它們的值將與它們對應的權重相乘,做爲圖中大圓的輸入。大圓的‘核’是一個函數,叫激勵函數(activation function),它把全部這些新的、通過權重調整後的輸入所有加起來,造成單個的激勵值(activation value)。激勵值也是一浮點數,且一樣可正可負。而後,再根據激勵值來產生函數的輸出也即神經細胞的輸出:若是激勵值超過某個閥值(做爲例子咱們假設 閥值爲1.0),就會產生一個值爲1的信號輸出;若是激勵值小於閥值1.0,則輸出一個0。這是人工神經細胞激勵函數的一種最簡單的類型。在這裏,從激勵 值產生輸出值是一個階躍函數[譯註]。看一看圖3後你就能猜到爲何有這樣的名稱。
圖3 階躍激勵函數
[譯註] 由圖可知階躍函數是一元的,而激勵函數既然能把多個輸入相加應爲多元,故需加以區別。
若是到目前爲止你對這些尚未得到不少感受,那也沒必要擔憂。竅門就是: 不要企圖去感受它,暫時就隨波逐流地跟我一塊兒向前走吧。在經歷本章的若干處後,你最終就會開始弄清楚它們的意義。而如今,就放鬆一點繼續讀下去吧。
3.1 如今須要一些數學了(Now for Some Math)
從此討論中,我將盡可能把數學下降到絕對少許,但學習一些數學記號對下面仍是頗有用的。我將把數學一點一點地餵給你,在到達有關章節時向你介紹一些新概 念。我但願採用這樣的方式能使你的頭腦能更溫馨地吸取全部的概念,並使你在開發神經網絡的每一個階段都能看到怎樣把數學應用到工做中。如今首先讓咱們來看一 看,怎樣把我在此以前告訴你的全部知識用數學方式表達出來。
一我的工神經細胞(從如今開始,我將把「人工神經細胞」簡稱它爲「神經細胞」) 能夠有任意n個輸入,n表明總數。能夠用下面的數學表達式來表明全部n個輸入:
x1, x2, x3, x4, x5, ..., xn
一樣 n 個權重可表達爲:
w1, w2, w3, w4, w5 ..., wn
請記住,激勵值就是全部輸入與它們對應權重的之乘積之總和,所以,如今就能夠寫爲:
a = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 +...+ wnxn
以這種方式寫下的求和式,我在第5章「創建一個更好的遺傳算法」中已提到,能夠用希臘字母Σ來簡化:
注:
神經網絡的各個輸入,以及爲各個神經細胞的權重設置,均可以看做一個n維的向量。你在許多技術文獻中經常能夠看到是以這樣的方式來引用的。
下面咱們來考察在程序中應該怎樣實現?假設輸入數組和權重數組均已初始化爲x[n]和w[n],則求和的代碼以下:
double activation = 0;
for(int i=0; i<n; ++i)
{
activation += x[i] * w[i];
}
圖4以圖形的方式表示了此方程。請別忘記,若是激勵值超過了閥值,神經細胞就輸出1; 若是激活小於閥值,則神經細胞的輸出爲0。這和一個生物神經細胞的興奮和抑制是等價的。咱們假設一個神經細胞有5個輸入,他們的權重w都初始化成正負1之 間的隨機值(-1 < w < 1) 。 表2說明了激勵值的求和計算過程。
圖4 神經細胞的激勵函數
若是咱們假定激活所需閥值=1,則因激勵值1.1 > 激活閥值1,因此這個神經細胞將輸出1。
在進一步讀下去以前,請你必定要確切弄懂激勵函數怎樣計算。
表2 神經細胞激勵值的計算
輸 入
|
權 重
|
輸入*權重的乘積
|
運行後總和
|
1
|
0.5
|
0.5
|
0.5
|
0
|
-0.2
|
0
|
0.5
|
1
|
-0.3
|
-0.3
|
0.2
|
1
|
0.9
|
0.9
|
1.1
|
0
|
0.1
|
0
|
1.1
|
3.2 行,我知道什麼是神經細胞了,但用它來幹什麼呢?
大腦裏的生物神經細胞和其餘的神經細胞是相互鏈接在一塊兒的。爲了建立一我的工神經網絡,人工神經細胞也要以一樣方式相互鏈接在一塊兒。爲此能夠有許多不一樣 的鏈接方式,其中最容易理解而且也是最普遍地使用的,就是如圖5所示那樣,把神經細胞一層一層地連結在一塊兒。這一種類型的神經網絡就叫前饋網絡 (feedforword network)。這一名稱的由來,就是由於網絡的每一層神經細胞的輸出都向前饋送(feed)到了它們的下一層(在圖中是畫在它的上面的那一層),直到 得到整個網絡的輸出爲止。
圖5 一個前饋網絡
由圖可知,網絡共有三層(譯註:輸入層不是神經細胞,神經細胞只有兩層)。輸入層中的每一個輸入都饋送到了隱藏層,做爲該層每個神經細胞的輸入;然 後,從隱藏層的每一個神經細胞的輸出都連到了它下一層(即輸出層)的每個神經細胞。圖中僅僅畫了一個隱藏層,做爲前饋網絡,通常地能夠有任意多個隱藏層。 但在對付你將處理的大多數問題時一層一般是足夠的。事實上,有一些問題甚至根本不須要任何隱藏單元,你只要把那些輸入直接連結到輸出神經細胞就好了。另 外,我爲圖5選擇的神經細胞的個數也是徹底任意的。每一層實際均可以有任何數目的神經細胞,這徹底取決於要解決的問題的複雜性。但神經細胞數目愈多,網絡 的工做速度也就愈低,因爲這一緣故,以及爲了其餘的幾種緣由(我將在第9章做出解釋),網絡的規模老是要求保持儘量的小。
到此我能想象你或許已對全部這些信息感到有些茫然了。我認爲,在這種狀況下,我能作的最好的事情,就是向你介紹一個神經網絡在現實世界中的實際應用例子,它有望使你本身的大腦神經細胞獲得興奮!不錯吧?好的,下面就來了...
你可能已聽到或讀到過神經網絡經常用來做模式識別。這是由於它們善於把一種輸入狀態(它所企圖識別的模式)映射到一種輸出狀態(它曾被訓練用來識別的模式)。
下面咱們來看它是怎麼完成的。咱們以字符識別做爲例子。設想有一個由8x8個格子組成的一塊麪板。每個格子裏放了一個小燈,每一個小燈均可獨立地被打開(格子變亮)或關閉(格子變黑),這樣面板就能夠用來顯示十個數字符號。圖6顯示了數字「4」。
圖6 用於字符顯示的矩陣格點
要解決這一問題,咱們必需設計一個神經網絡,它接收面板的狀態做爲輸入,而後輸出一個1或0;輸出1表明ANN確認已顯示了數字「4」,而輸出0表示沒 有顯示「4」。所以,神經網絡須要有64個輸入(每個輸入表明面板的一個具體格點) 和由許多神經細胞組成的一個隱藏層,還有僅有一個神經細胞的輸出層,隱藏層的全部輸出都饋送到它。我真但願你能在你的頭腦中畫出這個圖來,由於要我爲你把 全部這些小圓和連線通通畫出來確實不是一樁愉快的事<一笑>。
一旦神經網絡體系建立成功後,它必須接受訓練來認出數字 「4」。爲此可用這樣一種方法來完成:先把神經網的全部權重初始化爲任意值。而後給它一系列的輸入,在本例中,就是表明面板不一樣配置的輸入。對每一種輸入 配置,咱們檢查它的輸出是什麼,並調整相應的權重。若是咱們送給網絡的輸入模式不是「4」, 則咱們知道網絡應該輸出一個0。所以每一個非「4」字符時的網絡權重應進行調節,使得它的輸出趨向於0。當表明「4」的模式輸送給網絡時,則應把權重調整到 使輸出趨向於1。
若是你考慮一下這個網絡,你就會知道要把輸出增長到10是很容易的。而後經過訓練,就能夠使網絡能識別0到9 的全部數字。但爲何咱們到此中止呢?咱們還能夠進一步增長輸出,使網絡能識別字母表中的所有字符。這本質上就是手寫體識別的工做原理。對每一個字符,網絡 都須要接受許多訓練,使它認識此文字的各類不一樣的版本。到最後,網絡不單能認識已經訓練的筆跡,還顯示了它有顯著的概括和推廣能力。也就是說,若是所寫文 字換了一種筆跡,它和訓練集中全部字跡都略有不一樣,網絡仍然有很大概率來認出它。正是這種概括推廣能力,使得神經網絡已經成爲可以用於無數應用的一種無價 的工具,從人臉識別、醫學診斷,直到跑馬賽的預測,另外還有電腦遊戲中的bot(做爲遊戲角色的機器人)的導航,或者硬件的robot(真正的機器人)的 導航。
這種類型的訓練稱做有監督的學習(supervised learnig),用來訓練的數據稱爲訓練集(training set)。調整權重能夠採用許多不一樣的方法。對本類問題最經常使用的方法就是反向傳播(backpropagation,簡稱backprop或BP)方法。 有關反向傳播問題,我將會在本書的後面,當你已能訓練神經網絡來識別鼠標走勢時,再來進行討論。在本章剩餘部分我將集中注意力來考察另外的一種訓練方式, 即根本不須要任何導師來監督的訓練,或稱無監督學習(unsupervised learnig)。
這樣我已向你介紹了一些基本的知識,如今讓咱們來考察一些有趣的東西,並向你介紹第一個代碼工程。
遊戲編程中的人工智能技術
>
(連載之三)
4. 聰明的掃雷機工程(Smart Minesweeper Project)
我要向你介紹的第一個完整例子,是怎麼使用神經網絡來控制具備人工智能的掃雷機的行爲。掃雷機工做在一個很簡單的環境中,那裏只有掃雷機以及隨機散佈的許多地雷。

圖7 運行中的演示程序。
儘管書上圖形畫成了黑白色,但當你運行程序時性能最好的掃雷機將顯現爲紅色。地雷,你可能已經猜到,就是那些小方形。工程的目標是建立一個網絡,它不需 要從咱們這裏獲得任何幫助,就能本身進行演化(evolve)去尋找地雷。爲了實現這一功能,網絡的權重將被編碼到基因組中,並用一個遺傳算法來演化它 們。
怎麼樣,很酷吧?
提示(重要) 若是你跳過前面的一些章節來到這裏,而你又不瞭解怎樣使用遺 傳算法,則在進一步閱讀下面的內容以前,你應回到前面去補讀一下有關遺傳算法的內容。
|
首先讓我解釋人工神經網絡(ANN)的體系結構。咱們須要決定輸入的數目、輸出的數目、還有隱藏層和每一個隱藏層中隱藏單元的數目。
4.1 選擇輸出(Choosing the Outputs)
那麼,人工神經網絡怎樣控制掃雷機的行動呢?很好!咱們把掃雷機想象成和坦克車同樣,經過左右2個能轉動的履帶式輪軌(track)來行動的。見圖案9.8。
圖8 掃雷機的控制
掃雷機向前行進的速度,以及向左、向右轉彎的角度,都是經過改變2個履帶輪的相對速度來實現的。所以,神經網絡須要2個輸入,1個是左側履帶輪的速度,另外一個是右側履帶輪的速度。
啊,可是...,我聽見你在嘀咕了。若是網絡只能輸出一個1或一個0,咱們怎麼能控制車軌移動的快慢呢? 你是對的;若是利用之前描述的階躍函數來決定輸出,咱們就根本沒法控制掃雷機實際移動。幸虧,我有一套戲法,讓我捲起袖子來,把激勵函數的輸出由階躍式改 變成爲在0-1之間連續變化的形式,這樣就能夠供掃雷機神經細胞使用了。爲此,有幾種函數都能作到這樣,咱們使用的是一個被稱爲邏輯斯蒂S形函數 (logistic sigmoid function)[譯註1]。該函數所實現的功能,本質上說,就是把神經細胞原有的階躍式輸出曲線鈍化爲一光滑曲線,後者繞y軸0.5處點對稱[譯註 2],如圖9所示。
[譯註1] logistic有’計算的’或’符號邏輯的’等意思在內,和’邏輯的(logic)’意義不一樣。
[譯註2] 點對稱圖形繞對稱點轉180度後能與原圖重合。若f(x)以原點爲點對稱,則有f(-x)=-f(x)

圖9 S形曲線。
當神經細胞的激勵值趨於正、負無窮時,S形函數分別趨於1或0。負的激勵值對應的函數值都<0.5; 正激勵值對應的函數值都>0.5。S形函數用數學表達式寫出來則爲:
這個方程看上去可能會嚇唬一些人,但其實很簡單。e是數學常數,近似等於2.7183,a是神經細胞的激勵值,它是函數的自變量,而p是一個用來控制曲 線形狀變化快慢或陡峭性的參數。p一般設定爲1。當p賦以較大值時,曲線就顯得平坦,反之,就會使曲線變爲陡峭。見圖1O。很低的p值所生成的函數就和階 躍函數近似。P值的大小用來控制什麼時候使神經網絡由低變高開始翻轉有很大做用,可是在本例子中咱們將它保持爲1。
注:「S型」的英文原名Sigmoid 或Sigmoidal 原來是根據希臘字「Sigma」得來的,但很是巧它也能夠說成是曲線的一種形狀。

圖7。10 不一樣的S形響應曲線。
4.2 選擇輸入(Choosing the Inputs)
上面咱們已經把輸出安排好了,如今咱們來考慮輸入,肯定網絡須要什麼樣的輸入?爲此,咱們必須想象一下掃雷機的具體細節:須要什麼樣的信息才能使它朝地雷前進?你可能想到的第一個輸入信息清單是:
- 掃雷機的位置(x1,y1)
- 與掃雷機最靠近的地雷的位置(x2,y2)
- 表明掃雷機前進方向的向量(x3,y3)
這樣一共獲得6個輸入。可是,要網絡使用這些輸入,工做起來就很是困難,由於,網絡在像咱們但願的那樣執行工做以前,必須尋找全部6個輸入之間的數學關 系,而這有至關工做量。能夠把此做爲一個練習卻是很理想的:去試試如何給出最少數量的輸入而仍能爲網絡傳達解決問題所須要的所有信息。 你的網絡使用的輸入愈少,網絡所要求的神經細胞數目也愈少。而較少的神經細胞就意味更快速的訓練和更少的計算,有利於網絡更高速度的工做。
只要做少許的額外考慮,就可以把輸入的個數減小爲4,這就是圖11中所畫出的兩個向量的4個參數。
把神經網絡的全部輸入進行規範化是一種好想法。這裏的意思並非說每一個輸入都要改變大小使它們都在0~1間,而是說每個輸入應該受到同等重視。例如,拿 咱們已經討論過的掃雷機輸入爲例。瞄準向量或視線向量(look-at vector)老是一個規範化向量,即長度等於1,份量x和y都在0~1間。但從掃雷機到達其最近地雷的向量就可能很大,其中的一個份量甚至有可能和窗體 的寬度或高度同樣大。若是這個數據以它的原始狀態輸入到網絡,網絡對有較大值的輸入將顯得更靈敏,由此就會使網絡性能變差。所以,在信息輸入到神經網絡中 去以前,數據應預先定比(scaled)和標準化(standardized),使它們大小類似(similar)。在本特例中,由掃雷機引到與其最接近 地雷的向量須要進行規範化(normalized)。這樣能夠使掃雷機的性能獲得改良。

圖11 選擇輸入。
小技巧: 有時,你把輸入數據從新換算(rescale)一下,使它以0點爲中心,就能從你的神經網絡得到最好的性能。這一小竅門在你設計網絡時永遠值得一試。但我在掃雷機工程中沒有采用這一方法,這是由於我想使用一種更直覺的方法。
|
4.3 隱藏的神經細胞要多少?(How many Hidden Neurons?)
到此咱們已把輸入、輸出神經細胞的數目和種類肯定下來了,下一步是肯定隱藏層的數目,並肯定每一個隱藏層中神經細胞必須有多少?但遺憾的是,尚未一種確 切的規則可用來計算這些。它們的開發又須要憑我的的「感受」了。某些書上和文章中確實給過一些提綱性的東西,告訴你如何去決定隱藏神經細胞個數,但業內專 家們的一致見解是:你只能把任何建議看成不可全信的東西,主要還要靠本身的不斷嘗試和失敗中得到經驗。但你一般會發現,你所遇到的大多數問題都只要用一個 隱藏層就能解決。因此,本領的高低就在於如何爲這一隱藏層肯定最合適的神經細胞數目了。顯然,個數是愈少愈好,由於我前面已經說起,數目少的神經細胞可以 造就快速的網絡。一般,爲了肯定出一個最優總數,我老是在隱藏層中採用不一樣數目的神經細胞來進行試驗。我在本章所編寫的神經網絡工程的.
初版本中一共使用了10個隱藏神經細胞(固然,個人這個數字也不必定是最好的<一笑>)。你應圍繞這個數字的附近來作遊戲,並觀察隱藏層 神經細胞的數目對掃雷機的演化會產生什麼樣的影響。無論怎樣,理論已經夠了,讓咱們拿一個具體程序來看看吧!你能夠在本書所附光盤的 Chapter7/Smart Sweepers v1.0文件夾中找到本章下面幾頁即將描述的全部程序的源碼。
遊戲編程中的人工智能技術
.
4.4 CNeuralNet.h(神經網絡類的頭文件)
在CNeuralNet.h 文件中,咱們定義了人工神經細胞的結構、定義了人工神經細胞的層的結構、以及人工神經網絡自己的結構。首先咱們來考察人工神經細胞的結構。
4.4.1 SNeuron(神經細胞的結構)
這是很簡單的結構。人工神經細胞的結構中必須有一個正整數來紀錄它有多少個輸入,還須要有一個向量std:vector來表示它的權重。請記住,神經細胞的每個輸入都要有一個對應的權重。
Struct SNeuron
{
// 進入神經細胞的輸入個數
int m_NumInputs;
// 爲每一輸入提供的權重
vector<double> m_vecWeight;
//構造函數
SNeuron(int NumInputs);
};
如下就是SNeuron 結構體的構造函數形式:
SNeuron::SNeuron(int NumInputs): m_NumInputs(NumInputs+1)
(
// 咱們要爲偏移值也附加一個權重,所以輸入數目上要 +1
for (int i=0; i<NumInputs+1; ++i)
{
// 把權重初始化爲任意的值
m_vecWeight.push_back(RandomClamped());
}
}
由上能夠看出,構造函數把送進神經細胞的輸入數目NumInputs做爲一個變元,併爲每一個輸入建立一個隨機的權重。全部權重值在-1和1之間。
這是什麼? 我聽見你在說。
這裏多出了一個權重!不 錯,我很高興看到你能注意到這一點,由於這一個附加的權重十分重要。但要解釋它爲何在那裏,我必須更多地介紹一些數學知識。回憶一下你就能記得,激勵值 是全部輸入*權重的乘積的總和,而神經細胞的輸出值取決於這個激勵值是否超過某個閥值(t)。這能夠用以下的方程來表示:
w1x1 + w2x2 + w3x3 +...+ wnxn >= t
上式是使細胞輸出爲1的條件。由於網絡的全部權重須要不斷演化(進化),若是閥值的數據也能一塊兒演化,那將是很是重要的。要實現這一點不難,你使用一個簡單的詭計就可讓閥值變成權重的形式。從上面的方程兩邊各減去t,得:
w1x1 + w2x2 + w3x3 +...+ wnxn –t >= 0
這個方程能夠再換用一種形式寫出來,以下:
w1x1 + w2x2 + w3x3 +...+ wnxn + t *(–1) >= 0
到此,我但願你已能看出,閥值t爲何能夠想像成爲始終乘以輸入爲 -1的權重了。這個特殊的權重一般叫偏移(bias),這就是爲何每一個神經細胞初始化時都要增長一個權重的理由。如今,當你演化一個網絡時,你就沒必要再 考慮閥值問題,由於它已被內建在權重向量中了。怎麼樣,想法不錯吧?爲了讓你心中絕對敲定你所學到的新的人工神經細胞是什麼樣子,請再參看一下圖12。
圖12 帶偏移的人工神經細胞。
4.4.2 SNeuronLayer(神經細胞層的結構)
神經細胞層SNeuronLayer的結構很簡單;它定義了一個如圖13中所示的由虛線包圍的神經細胞SNeuron所組成的層。
圖13 一個神經細胞層。
如下就是層的定義的源代碼,它應該再也不須要任何進一步的解釋:
struct SNeuronLayer
{
// 本層使用的神經細胞數目
int m_NumNeurons;
// 神經細胞的層
vector<SNeuron> m_vecNeurons;
SNeuronLayer(int NumNeurons, int NumInputsPerNeuron);
};
4.4.3 CNeuralNet(神經網絡類)
這是建立神經網絡對象的類。讓咱們來通讀一下這一個類的定義:
class CNeuralNet
{
private:
int m_NumInputs;
int m_NumOutputs;
int m_NumHiddenLayers;
int m_NeuronsPerHiddenLyr;
// 爲每一層(包括輸出層)存放全部神經細胞的存儲器
vector<SNeuronLayer> m_vecLayers;
全部private成員由其名稱容易獲得理解。須要由本類定義的就是輸入的個數、輸出的個數、隱藏層的數目、以及每一個隱藏層中神經細胞的個數等幾個參數。
public:
CNeuralNet();
該構造函數利用ini文件來初始化全部的Private成員變量,而後再調用CreateNet來建立網絡。
// 由SNeurons建立網絡
void CreateNet();
我過一下子立刻就會告訴你這個函數的代碼。
// 從神經網絡獲得(讀出)權重
vector<double> GetWeights()const;
因爲網絡的權重須要演化,因此必須建立一個方法來返回全部的權重。這些權重在網絡中是以實數型向量形式表示的,咱們將把這些實數表示的權重編碼到一個基因組中。當我開始談論本工程的遺傳算法時,我將爲您確切說明權重如何進行編碼。
// 返回網絡的權重的總數
int GetNumberOfWeights()const;
// 用新的權重代替原有的權重
void PutWeights(vector<double> &weights);
這一函數所作的工做與函數GetWeights所作的正好相反。當遺傳算法執行完一代時,新一代的權重必須從新插入神經網絡。爲咱們完成這一任務的是PutWeight方法。
// S形響應曲線
inline double Sigmoid(double activation, double response);
當已知一個神經細胞的全部輸入*重量的乘積之和時,這一方法將它送入到S形的激勵函數。
// 根據一組輸入,來計算輸出
vector<double> Update(vector<double> &inputs);
對此Update函數函數我立刻就會來進行註釋的。
}; // 類定義結束
4.4.3.1 CNeuralNet::CreateNet(建立神經網絡的方法)
我在前面沒有對CNeuralNet的2個方法加以註釋,這是由於我要爲你顯示它們的更完整的代碼。這2個方法的第一個是網絡建立方法 CreateNet。它的工做就是把由細胞層SNeuronLayers所收集的神經細胞SNeurons聚在一塊兒來組成整個神經網絡,代碼爲:
void CNeuralNet::CreateNet()
{
// 建立網絡的各個層
if (m_NumHiddenLayers > 0)
{
//建立第一個隱藏層[譯註]
m_vecLayers.push_back(SNeuronLayer(m_NeuronsPerHiddenLyr,
m_NumInputs));
for( int i=O; i<m_NumHiddenLayers-l; ++i)
{
m_vecLayers.push_back(SNeuronLayer(m_NeuronsPerHiddenLyr,
m_NeuronsPerHiddenLyr));
}
[譯註]若是容許有多個隱藏層,則由接着for循環即能建立其他的隱藏層。
// 建立輸出層
m_vecLayers.push_back(SNeuronLayer(m_NumOutput,m_NeuronsPerHiddenLyr));
}
else //無隱藏層時,只需建立輸出層
{
// 建立輸出層
m_vecLayers.push_back(SNeuronLayer(m_NumOutputs, m_NumInputs));
}
}
4.4.3.2 CNeuralNet::Update(神經網絡的更新方法)
Update函數(更新函數)稱得上是神經網絡的「主要勞動力」了。這裏,輸入網絡的數據input是以雙精度向量std::vector的數據格式傳 遞進來的。Update函數經過對每一個層的循環來處理輸入*權重的相乘與求和,再以所得的和數做爲激勵值,經過S形函數來計算出每一個神經細胞的輸出,正如 咱們前面最後幾頁中所討論的那樣。Update函數返回的也是一個雙精度向量std::vector,它對應的就是人工神經網絡的全部輸出。
請你本身花兩分鐘或差很少的時間來熟悉一下以下的Update函數的代碼,這能使你正確理解咱們繼續要講的其餘內容:
vector<double> CNeuralNet::Update(vector<double> &inputs)
{
// 保存從每一層產生的輸出
vector<double> outputs;
int cWeight = 0;
// 首先檢查輸入的個數是否正確
if (inputs.size() != m_NumInputs)
{
// 若是不正確,就返回一個空向量
return outputs;
}
// 對每一層,...
for (int i=0; i<m_NumHiddenLayers+1; ++i)
{
if (i>O)
{
inputs = outputs;
}
outputs.clear();
cWeight = 0;
// 對每一個神經細胞,求輸入*對應權重乘積之總和。並將總和拋給S形函數,以計算輸出
for (int j=0; j<m_vecLayers[i].m_NumNeurons; ++j)
{
double netinput = 0;
int NumInputs = m_vecLayers[i].m_vecNeurons[j].m_NumInputs;
// 對每個權重
for (int k=O; k<NumInputs-l; ++k)
{
// 計算權重*輸入的乘積的總和。
netinput += m_vecLayers[i].m_vecNeurons[j].m_vecWeight[k] *
inputs[cWeight++];
}
// 加入偏移值
netinput += m_vecLayers[i].m_vecNeurons[j].m_vecWeight[NumInputs-1] *
CParams::dBias;
別忘記每一個神經細胞的權重向量的最後一個權重實際是偏移值,這咱們已經說明過了,咱們老是將它設置成爲 –1的。我已經在ini文件中包含了偏移值,你能夠圍繞它來作文章,考察它對你建立的網絡的功能有什麼影響。不過,這個值一般是不該該改變的。
// 每一層的輸出,當咱們產生了它們後,咱們就要將它們保存起來。但用Σ累加在一塊兒的
// 激勵總值首先要經過S形函數的過濾,才能獲得輸出
outputs.push_back(Sigmoid(netinput,CParams::dActivationResponse)); cWeight = 0:
}
}
return outputs;
}
遊戲編程中的人工智能技術
.
4.5 神經網絡的編碼(Encoding the Network)
在本書的開始幾章中,你已經看到過怎樣用各類各樣的方法爲遺傳算法編碼。但當時我並無向你介紹過
一個用實數編碼的具體例子,由於我知道我要留在這裏向你介紹。我曾經講到,爲了設計一個前饋型神經網絡,
編碼是很容易的。咱們從左到右讀每一層神經細胞的權重,讀完第一個隱藏層,再向上讀它的下一層,把所讀
到的數據依次保存到一個向量中,這樣就實現了網絡的編碼。所以,若是咱們有圖14所示的網絡,則它的權重
編碼向量將爲:
0.3, -O.8, -O.2, 0.6, O.1, -0.l, 0.4, 0.5
在這一網絡中,爲了簡單,我沒有把偏移值的權重包括進去。但在實際實現編碼時,你必須包含偏移值這
個權重,不然你確定沒法得到你所須要的結果。
圖14 爲權重編碼。
在此以前講的事情你都懂了嗎?好極了,那下面就讓咱們轉來考慮,怎樣用遺傳算法來操縱已編碼的基因吧。
4.6 遺傳算法(The Genetic Algorithm)
到此,全部的權重已經象二進制編碼的基因組那樣,造成了一個串,咱們就能夠象本書早先討論過的那樣
來應用遺傳算法了。遺傳算法(GA)是在掃雷機已被容許按照用戶指定的幀數(爲了某種緣故, 我下面更喜歡
將幀數稱做滴答數,英文是ticks)運轉後執行的。你能夠在ini文件中找到這個滴答數(iNumTicks)的設置。
下面是基因組結構體的代碼。這些對於你應該是十分面熟的東西了。
Struct SGenome
{
vector <double> vecWeights;
double dFitness;
SGenome():dFitness(0) {}
SGenome(vector <double> w, double f):vecWeights(w),dFitness(f){}
//重載'<'的排序方法
friend bool operator<(const SGenome& lhs, const SGenome& rhs)
{
return (lhs.dFitness < rhs.dFitness);
}
};
從上面的代碼你可看出,這一SGenome結構和咱們在本書全部其餘地方見到的SGenome結構幾乎徹底一致,惟一的差異就是這裏的染色體是一個雙精度向量std::vector。所以,能夠和一般同樣來應用雜交操做和選擇
操做。但突變操做則稍微有些不一樣,這裏的權重值是用一個最大值爲dMaxPerturbation的隨機數來搔擾的。這一
參數dMaxPerturbation在ini文件中已做了聲明。另外,做爲浮點數遺傳算法,突變率也被設定得更高些。在本工
程中,它被設成爲0.1。
下面就是掃雷機工程遺傳算法類中所見到的突變函數的形式:
void CGenAlg::Mutate(vector<double> &chromo)
{
// 遍歷權重向量,按突變率將每個權重進行突變
for (int i=0; i<chromo.size(); ++i)
{
// 咱們要騷擾這個權重嗎?
if (RandFloat() < m_dMutationRate)
{
// 爲權重增長或減少一個小的數量
chromo[i] += (RandomClamped() * CParams::dMaxPerturbatlon);
}
}
}
如同之前的工程那樣,我已爲v1.0版本的Smart Minesweepers工程保留了一個很是簡單的遺傳算法。這樣
就能給你留下許多餘地,可以讓你利用之前學到的技術來改進它。就象大多數別的工程同樣,v1.O版只用輪盤賭
方式選精英,並採用單點式雜交。
注意:
當程序運行時,權重能夠被演化成爲任意的大小,它們不受任何形式的限制。
4.7 掃雷機類(The CMinesweeper Class)
這一個類用來定義一個掃雷機。就象上一章描述的登月艇類同樣,掃雷機類中有一個包含了掃雷機位置、
速度、以及如何轉換方向等數據的紀錄。類中還包含掃雷機的視線向量(look-at vector);它的2個份量被用
來做爲神經網絡的2個輸入。這是一個規範化的向量,它是在每一幀中根據掃雷機自己的轉動角度計算出來的,
它指示了掃雷機當前是朝着哪個方向,如圖11所示。
下面就是掃雷機類CMinesweeper的聲明:
class CMinesweeper
{
private:
// 掃雷機的神經網絡
CNeuralNet m_ItsBrain;
// 它在世界座標裏的位置
SVector2D m_vPosition;
// 掃雷機面對的方向
SVector2D m_vLookAt;
// 它的旋轉(surprise surprise)
double m_dRotation;
double m_dSpeed;
// 根據ANN保存輸出
double m_lTrack,
m_rTrack;
m_lTrack和m_rTrack根據網絡保存當前幀的輸出。
這些就是用來決定掃雷機的移動速率和轉動角度的數值。
// 用於度量掃雷機適應性的分數
double m_dFitness;
每當掃雷機找到一個地雷,它的適應性分數就要增長。
// 掃雷機畫出來時的大小比例
double m_dScale;
// 掃雷機最鄰近地雷的下標位置
int m_iClosestMine;
在控制器類CControl1er中,有一個屬於全部地雷的成員向量std::vector。
而m_iClosestMine就是表明最靠近掃雷機的那個地雷在該向量中的位置的下標。
public:
CMinesweeper();
// 利用從掃雷機環境獲得的信息來更新人工神經網
bool Update(vector<SVector2D> &mines);
// 用來對掃雷機各個頂點進行變換,以便接着能夠畫它出來
void WorldTransform(vector<SPoint> &sweeper);
// 返回一個向量到最鄰近的地雷
5Vector2D GetClosestMine(vector<SVector2D> &objects);
// 檢查掃雷機看它是否已經發現地雷
int CheckForMine(vector<SVector2D> &mines, double size);
void Reset();
// ----------------- 定義各類供訪問用的函數
SVector2D Position()const { return m_vPosition; }
void IncrementFitness(double val) { m_dFitness += val; }
double Fitness()const { return m_dFitness; }
void PutWeights(vector<double> &w) { m_ItsBrain.PutWeights(w); }
int GetNumberOfWeights()const
{ return m_ItsBrain.GetNumberOfWeights(); }
};
4.7.1 The CMinesweeper::Update Function(掃雷機更新函數)
須要更詳細地向你說明的CMinesweeper類的方法只有一個,這就是Update更新函數。該函數在每一幀中
都要被調用,以更新掃雷機神經網絡。讓咱們考察這函數的肚子裏有些什麼貨色:
bool CMinesweeper::Update(vector<SVector2D> &mines)
{
//這一貫量用來存放神經網絡全部的輸入
vector<double> inputs;
//計算從掃雷機到與其最接近的地雷(2個點)之間的向量
SVector2D vClosestMine = GetClosestMine(mines);
//將該向量規範化
Vec2DNormalize(vClosestMine);
首先,該函數計算了掃雷機到與其最靠近的地雷之間的向量,而後使它規範化。(記住,向量規範化後它
的長度等於1。)但掃雷機的視線向量(look-at vector)這時不須要再做規範化,由於它的長度已經等於1了。
因爲兩個向量都有效地化成了一樣的大小範圍,咱們就能夠認爲輸入已是標準化了,這我前面已講過了。
//加入掃雷機->最近地雷之間的向量
Inputs.push_back(vClosestMine.x);
Inputs.push_back(vCIosestMine.y);
//加入掃雷機的視線向量
Inputs.push_back(m_vLookAt.x);
Inputs.push_back(m_vLookAt.y);
//更新大腦,並從網絡獲得輸出
vector<double> output = m_ItsBrain.Update(inputs);
而後把視線向量,以及掃雷機與它最接近的地雷之間的向量,都輸入到神經網絡。函數CNeuralNet::Update利
用這些信息來更新掃雷機網絡,並返回一個std::vector向量做爲輸出。
//保證在輸出的計算中沒有發生錯誤
if (output.size() < CParams::iNumOutputs)
{
return false;
}
// 把輸出賦值到掃雷機的左、右輪軌
m_lTrack = output[0];
m_rTrack = output[1];
在更新神經網絡時,當檢測到確實沒有錯誤時,程序把輸出賦給m_lTrack和m_rTrack。 這些值表明施加
到掃雷機左、右履帶輪軌上的力。
// 計算駕駛的力
double RotForce = m_lTrack - m_rTrack;
// 進行左轉或右轉
Clamp(RotForce, -CParams::dMaxTurnRate, CParams::dMaxTurnRate);
m_dSpeed = (m_lTrack + m_rTrack);
掃雷機車的轉動力是利用施加到它左、右輪軌上的力之差來計算的。並規定,施加到左軌道上的力減去施
加到右軌道上的力,就獲得掃雷機車輛的轉動力。而後就把此力施加給掃雷機車,使它實行不超過ini文件所規
定的最大轉動率的轉動。而掃雷機車的行進速度不過就是它的左側輪軌速度與它的右側輪軌速度的和。既然我
們知道了掃雷機的轉動力和速度,它的位置和偏轉角度也就都能更新了。
//更新掃雷機左右轉向的角度
m_dRotation += RotForce;
// 更新視線角度
m_vLookAt.x = -sin(m_dRotation);
m_vLookAt.y = cos(m_dRotation);
// 更新它的位置
m_vPosition += (m_vLookAt* m_dSpeed);
// 若是掃雷機到達窗體四周,則讓它實行環繞,使它不至於離開窗體而消失
If (m_vPosition.x > CParams::WindowWidth) m_vPosition.x = 0;
If (m_vPosition.x < 0) m_vPosition.x = CParams::WindowWidth;
If (m_vPosition.y > CParams::WindowHeight) m_vPosition.y = 0;
If (m_vPosition.y < D) m_vPosition.y = CParams::WindowHeight;
爲了使事情儘量簡單,我已讓掃雷機在碰到窗體邊框時就環繞折回(wrap)。採用這種方法程序就再也不需
要作任何碰撞-響應方面的工做。環繞一塊空地打轉對咱們人來講是一樁很是難以想象的動做,但對掃雷機,這
就像池塘中的鴨子。
Returen true;
}
4.8 CController Class(控制器類)
CController類是和一切都有聯繫的類。圖15指出了其餘的各個類和CController類的關係。
下面就是這個類的定義:
class CController
{
private:
// 基因組羣體的動態存儲器(一個向量)
vector<SGenome> m_vecThePopulation;
圖15 minesweeper工程的程序流程圖
// 保存掃雷機的向量
vector<CMinesweeper> m_vecSweepers;
// 保存地雷的向量
vector<SVector2D> m_vecMines;
// 指向遺傳算法對象的指針
CGenAIg* m_pGA;
int m_NumSweepers;
int m_NumMines;
// 神經網絡中使用的權重值的總數
int m_NumWeightsInNN;
// 存放掃雷機形狀各頂點的緩衝區
vector<SPoint> m_SweeperVB;
// 存放地雷形狀各頂點的緩衝區
vector<SPoint> m_MineVB;
// 存放每一代的平均適應性分數,供繪圖用
vector<double> m_vecAvFitness;
// 存放每一代的最高適應性分
vector<double> m_vecBestFitness;
// 咱們使用的各類不一樣類型的畫筆
HPEN m_RedPen;
HPEN m_BluePen;
HPEN m_GreenPen;
HPEN m_OldPen;
// 應用程序窗口的句柄
HWND m_hwndMain;
// 切換掃雷機程序運行的速度
bool m_bFastRender;
// 每一代的幀數(滴答數)
int m_iTicks;
// 代的計數
int m_iGenerations;
// 窗體客戶區的大小
int cxClient,cyClient;
// 本函數在運行過程當中畫出具備平均-,和最優適應性值的圖
void PlotStats(HDC surface);
public:
CController(HWND hwndMain);
~CController();
void Render(HDC surface);
void WorldTransform(vector<SPoint> &VBuffer,
SVector2D vPos);
bool Update();
// 幾個公用的訪問方法
bool FastRender() { return m_bFastRender; }
void FastRender(bool arg){ m_bFastRender = arg; }
void FastRenderToggle() { m_bFastRender = !m_bFastRender; }
};
當建立CController類的某個實例時,會有一系列的事情發生:
建立CMinesweeper對象。
統計神經網絡中所使用的權重的總數,而後此數字即被利用來初始化遺傳算法類的一個實例。
從遺傳算法對象中隨機提取染色體(權重)並(利用細心的腦外科手術)插入到掃雷機的經網絡中。
建立了大量的地雷並被隨機地散播到各地。
爲繪圖函數建立了全部須要用到的GDI畫筆。
爲掃雷機和地雷的形狀建立了頂點緩衝區。
全部的一切現都已完成初始化,由此Update方法就能在每一幀中被調用來對掃雷機進行演化。
4.8.1 CController::Update Method(控制器的更新方法)
控制器更新方法CController::Update方法(或函數)在每一幀中都要被調用。當調用update函數時,函數
的 前一半經過對全部掃雷機進行循環,如發現某一掃雷機找到了地雷,就update該掃雷機的適應性分數。因爲m_vecThePopulation包含了所 有基因組的拷貝,相關的適應性分數也要在這時進行調整。若是爲完成一個代(generation)所須要的幀數均已經過,本方法就執行一個遺傳算法的時代 (epoch)來產生新一代的權重。這些
權重被用來代替掃雷機神經網絡中原有的舊的權重,使掃雷機的每個參數被從新設置,從而爲進入新一generation作好準備。
bool CController::Update()
{
// 掃雷機運行總數爲CParams::iNumTicks次的循環。在此循環週期中,掃雷機的神經網絡
// 不斷利用它周圍特有的環境信息進行更新。而從神經網絡獲得的輸出,使掃雷機實現所需的
// 動做。若是掃雷機碰見了一個地雷,則它的適應性將相應地被更新,且一樣地更新了它對應
// 基因組的適應性。
if (m_iTicks++ < CParams::iNumTicks)
{
for (int i=O; i<m_NumSweepers; ++i)
{
//更新神經網絡和位置
if (!m_vecSweepers[i].Update(m_vecMines))
{
//處理神經網絡時出現了錯誤,顯示錯誤後退出
MessageBox(m_hwndMain, 'Wrong amount of NN inputs!",
"Error", MB_OK);
return false;
}
// 檢查這一掃雷機是否已經發現地雷
int GrabHit = m_vecSweepers[i].CheckForMine(m_vecMines,
CParams::dMineScale);
if (GrabHit >= 0)
{
// 掃雷機已找到了地雷,因此要增長它的適應性分數
m_vecSweepers[i].IncrementFitness();
// 去掉被掃雷機找到的地雷,用在隨機位置放置的一個新地雷來代替
m_vecMines[GrabHit] = SVector2D(RandFloat() * cxClient,
RandFloat() * cyClient);
}
// 更新基因組的適應性值
m-vecThePopulation[i].dFitness = m_vecSweepers[i].Fitness();
}
}
// 一個代已被完成了。
// 進入運行遺傳算法並用新的神經網絡更新掃雷機的時期
else
{
// 更新用在咱們狀態窗口中狀態
m_vecAvFitness.push_back(m_pGA->AverageFitness());
m_vecBestFitness.push_back(m_pGA->BestFitness());
// 增長代計數器的值
++m_iGenerations;
// 將幀計數器復位
m_iTicks = 0;
// 運行GA建立一個新的羣體
m-vecThePopulation = m_pGA->Epoch(m_vecThePopulation);
// 在各掃雷機中重新插入新的(有但願)被改進的大腦
// 並將它們的位置進行復位,等
for(int i=O; i<m_NumSweepers; ++i)
{m_vecSweepers[i].m_ItsBrain.PutWeights(m_vecThePopulation[i].vecWeights);
m_vecSweepers[i].Reset();
}
}
returen true;
}
歸納起來,程序爲每一世代作的工做是:
l.爲全部掃雷機和爲iNumTicks個幀組織循環,調用Update函數
並根據狀況增長掃雷機適應值的得分。
2.從掃雷機神經網絡提取權重向量。
3.用遺傳算法去演化出一個新的網絡權重羣體。
4.把新的權重插入到掃雷機神經網絡。
5.轉到第1步進行重複,直到得到理想性能時爲止。
最後,表3列出了Smart Sweepers工程 v1.0版全部缺省參數的設置值。
表3 Smart Sweepers v1.0工程的缺省設置
神經網絡
|
參 數
|
設 置 值
|
輸入數目
|
4
|
輸出數目
|
2
|
隱藏層數目
|
1
|
隱藏層神經元數目
|
10
|
激勵響應
|
1
|
|
遺 傳 算 法
|
參 數
|
設 置 值
|
羣體大小
|
30
|
選擇類型
|
旋轉輪
|
雜交類型
|
單點
|
雜交率
|
0.7
|
突變率
|
0.1
|
精英設置(on/off)
|
On
|
精英數目(N/copies)
|
4/1
|
|
總 體 特 性
|
參 數
|
設 置 值
|
每時代的幀數
|
2000
|
4.9 運行此程序 (Running the Program)
當你運行程序時,「F」鍵用來切換2種不一樣的顯示狀態,一種是顯示掃雷機怎樣學習尋找地雷,一種是
示在運行期中產生的最優的與平均的適當性分數的統計圖表。 當顯示圖表時,程序將會加速運行。
遊戲編程中的人工智能技術
.
>
.
(連載之六)
4.10 功能的兩個改進 (A Couple of Improvements)
儘管掃雷機學習尋找地雷的本領十分不錯,這裏我仍有兩件事情要告訴你,它們能進一步改進掃雷機的性能。
4.10.1 改進一(Improvement Number One)
首先,單點crossover算子留下了許多可改進的餘地。按照它的規定,算子是沿着基因組長度任意地方切開的,這樣常有可能使個別神經細胞的基因組在權重的中間被一刀兩段地分開。
爲清楚起見,咱們來考察圖16的權重。這是咱們之前在說明基因組如何編碼時看過的一個簡單網絡。 在這
裏,雜交算子能夠沿向量長度的任意一處切開,這樣,就會有極大概率在某個神經細胞(如第二個)的權重中
間斷開,也就是說,在權重0.6和-0.1之間某處切開。這可能不會是咱們想要的,由於,若是咱們把神經細胞做
爲一個完整的單元來看待,則它在此之前所得到的任何改良就要被騷擾了。事實上,這樣的雜交操做有可能非
常很是象斷裂性突變(disruptive mutation)操做所起的做用。
圖16 簡單的網絡
與此針鋒相對,我已建立了另外一種類型的雜交運算,它只在神經細胞的邊界上進行切開。在圖16的例子中,
就是在第三、4或第六、7的兩個基因之間切開,如小箭頭所示。 爲了實現這一算法,我已在CNeuralNet類中補
充了另外一個切割方法: CalculateSplitPoints。這一方法建立了一個用於保存全部網絡權重邊界的矢量,它的代
碼以下:
vector<int> CNeuralNet::CalculateSplitPoints() const
{
vector<int> SplitPoints;
int WeightCounter = 0;
// 對每一層
for (int i=O; i<m_NumHiddenLayers + 1; ++i)
{
// 對每個神經細胞
for (int j=O; j<m_vecLayers[i].m_NumNeurons; ++j)
{
// 對每個權重
for (int k=O; k<m_vecLayers[i].m_vecNeurons[j].m_NumInputs; ++k)
{
++WeightCounter;
}
SplitPoints.push_back(WeightCounter - 1);
}
}
return SplitPoints;
}
這一方法是CController類構造函數在建立掃雷機並把斷裂點向量傳遞給遺傳算法類時調用的。它們被存儲
在一個名叫m_vecSplitPoints的std::vector向量中。而後遺傳算法就利用這些斷裂點來實現兩點雜交操做,其代
碼以下:
void CGenAlg::CrossoverAtSplits(const vector<double> &mum,
const vector<double> &dad,
vector<double> &babyl,
vector<double> &baby2)
{
// 若是超過了雜交率,就再也不進行雜交,把2個上代做爲2個子代輸出
// 若是2個上輩相同,也把它們做爲2個下輩輸出
if ( (RandFloat() > m_dCrossoverRate) || (mum == dad))
{
baby1 = mum;
baby2 = dad;
return;
}
// 肯定雜交的2個斷裂點
int index1 = RandInt(0, m_vecSplitPoints.size()-2);
int index2 = RandInt(Index1, m_vecSplitPoints.size()-1);
int cp1 = m_vecSplitPoints[Index1];
int cp2 = m_vecSplitPoints[Index2];
// 建立子代
for (int i=0; i<mum.size(); ++i)
{
if ( (i<cp1) || (i>=cp2) )
{
// 若是在雜交點外,保持原來的基因
babyl.push_back(mum[i]);
baby2.push_back(dad[i]);
}
else
{
// 把中間段進行交換
baby1.push_back(dad[1]);
baby2.push_back(mum[1]);
}
}
return;
}
根據個人經驗,我已發現,在進行雜交時,把神經細胞看成一個不可分割的單位,比在染色體長度上任意
一點分裂基因組,能獲得更好的結果。
4.10.2 改進二(Improvement Number Two)
我想和你討論的另外一個性能改進,是用另外一種方式來觀察網絡的那些輸入。在你已看到的例子中,咱們爲
網絡使用了4個輸入參數: 2個用於表示掃雷機視線方向的向量,另外2個用來指示掃雷機與其最靠近的地雷的方
向的向量。然而,有一種辦法,能夠把這些參數的個數減小到只剩下一個。
其實你想想就可知道,掃雷機爲了肯定地雷的位置,只要知道從它當前的位置和朝向出發,須要向左或
向右轉動多大的一個角度這一簡單的信息就夠了(若是你已經考慮到了這一點,那我在這裏要順便向您道賀了)。
因爲咱們已經計算了掃雷機的視線向量和從它到最鄰近地雷的向量,再來計算它們之間的角度(θ)應是一件極爲
簡單的事情 – 這就是這兩個向量的點積,這咱們在第6章「使登錄月球容易一點」中已討論過。見圖17。
圖17 計算到最鄰近地雷的轉動角度。
不幸的是,點積僅僅給出角度的大小; 它不能指示這一角度是在掃雷機的那一側。所以,我已寫了另外一個向
量函數返回一個向量相對於另外一個向量的正負號。該函數的原型以下所示:
inline int Vec2DSign(SVector2D &v1,SVector2D &v2);
若是你對它的機理感興趣,你能夠在文件SVector2D.h中找到它的源碼。但它的基本點就是: 若是v1至v2是
按順時針方向轉的,則函數返回 +1;若是v1至v2是按逆時針方向轉,則函數返回 -1。
把點積和Vec2Dsign兩者聯合起來,就能把輸入的精華提純出來,使網絡只需接受一個輸入就好了。下面
就是新的CMinesweeper::Update函數有關段落的代碼形式:
// 計算到最鄰近地雷的向量
SVector2D vClosestMine = GetClosestMine(mines);
// 將它規範化
Vec2DNormalize(vClosestMine);
// 計算掃雷機視線向量和它到最鄰近地雷的向量的點積。它給出了咱們要面對
// 最鄰近地雷所需轉動的角度
double dot = Vec2DDot(m_vLookAt, vClosestMine);
// 計算正負號
int sign = Vec2DSign(m_vLookAt, vClosestMine);
Inputs.push_back(dot*sign);
運行一下光盤Chapter7/Smart Sweepers v1.1目錄下的可執行程序executable,你就知道通過以上2個改
進,能爲演化過程提速多少。
須要注意的一樁重要事情是,帶有4個輸入的網絡要花很長時間進行演化,由於它必須在各輸入數據之間找
出更多的關係才能肯定它應如何行動。事實上,網絡實際就是在學習怎麼作點積並肯定它的正負極性。所以,當
你設計本身的網絡時,你應仔細權衡一下,是由你本身預先來計算許多輸入數據好呢(它將使CPU負擔增長,但
致使進化時間加快)仍是讓網絡來找輸入數據之間的複雜關係好(它將使演化時間變長,但能使CPU減小緊張)?
5 結束語(last words)
我但願你已享受到了你第一次攻入神經網絡這一奇妙世界的快樂。我打賭你必定在爲如此簡單就能使用它
們而感到驚訝吧,對嗎?我想我是猜對了。
在下面幾章裏我將要向你介紹更多的知識,告訴你一些新的訓練手段和演繹神經網絡結構的更多的方法。
但首先請你利用本章下面的提示去玩一下游戲是有意義的。
6 練習題 (Stuff to Try)
1。 在v1.0中,不用look-at向量做爲輸入,而改用旋轉角度θ做爲輸入,由此就能夠使網絡的輸入個數減小
成爲1個。請問這對神經網絡的演化有什麼影響?你對此的見解怎樣?
2。 試以掃雷機的位置(x1,y1)、和掃雷機最接近的地雷的位置(x2,y2)、以及掃雷機前進方向的向量
(x3,y3)等6個參數做爲輸入,來設計一個神經網絡,使它仍然可以演化去尋找地雷。
3。 改變激勵函數的響應。試用O.1 - O.3 之間的低端值,它將產生和階躍函數很是相像的一種激勵函數。
而後再試用高端值,它將給出較爲平坦的響應曲線。考察這些改變對演化進程具備什麼影響?
4。 改變神經網絡的適應性函數,使得掃雷機不是去掃除地雷,而是要演化它,使它能避開地雷。
5。 理一理清楚有關遺傳算法的各類不一樣設置和運算中使你感到模糊的東西!
6。 加入其餘的對象類型,好比人。給出一個新環境來演化掃雷機,使它能避開人,但照樣能掃除地雷。
(這可能沒有你想象那麼容易!)
Posted on
2011-03-07 22:30 蒼梧 閱讀(48399) 評論(
本文主要內容包括: (1) 介紹神經網絡基本原理,(2)AForge.NET實現前向神經網絡的方法,(3) Matlab實現前向神經網絡的方法 。編程
第0節、引例 數組
本文以Fisher的Iris數據集做爲神經網絡程序的測試數據集。Iris數據集能夠在http://en.wikipedia.org/wiki/Iris_flower_data_set 找到。這裏簡要介紹一下Iris數據集:網絡
有一批Iris花,已知這批Iris花可分爲3個品種,現須要對其進行分類。不一樣品種的Iris花的花萼長度、花萼寬度、花瓣長度、花瓣寬度會有差別。咱們現有一批已知品種的Iris花的花萼長度、花萼寬度、花瓣長度、花瓣寬度的數據。架構
一種解決方法是用已有的數據訓練一個神經網絡用做分類器。dom
若是你只想用C#或Matlab快速實現神經網絡來解決你手頭上的問題,或者已經瞭解神經網絡基本原理,請直接跳到第二節——神經網絡實現。ide
第一節、神經網絡基本原理 函數
1. 人工神經元(Artificial Neuron )模型
人工神經元是神經網絡的基本元素,其原理能夠用下圖表示:

圖1. 人工神經元模型
圖中x1~xn是從其餘神經元傳來的輸入信號,wij表示表示從神經元j到神經元i的鏈接權值,θ表示一個閾值 ( threshold ),或稱爲偏置( bias )。則神經元i的輸出與輸入的關係表示爲:


圖中yi表示神經元i的輸出,函數f稱爲激活函數 ( Activation Function )或轉移函數 ( Transfer Function ) ,net稱爲淨激活(net activation)。若將閾值當作是神經元i的一個輸入x0的權重wi0,則上面的式子能夠簡化爲:


若用X表示輸入向量,用W表示權重向量,即:
X = [ x0 , x1 , x2 , ....... , xn ]
則神經元的輸出能夠表示爲向量相乘的形式:


若神經元的淨激活net爲正,稱該神經元處於激活狀態或興奮狀態(fire),若淨激活net爲負,則稱神經元處於抑制狀態。
圖1中的這種「閾值加權和」的神經元模型稱爲M-P模型 ( McCulloch-Pitts Model ),也稱爲神經網絡的一個處理單元( PE, Processing Element )。
2. 經常使用激活函數
激活函數的選擇是構建神經網絡過程當中的重要環節,下面簡要介紹經常使用的激活函數。
(1) 線性函數( Liner Function )

(2) 斜面函數( Ramp Function )

(3) 閾值函數( Threshold Function )

以上3個激活函數都屬於線性函數,下面介紹兩個經常使用的非線性激活函數。
(4) S形函數( Sigmoid Function )

該函數的導函數:

(5) 雙極S形函數

該函數的導函數:

S形函數與雙極S形函數的圖像以下:

圖3. S形函數與雙極S形函數圖像
雙極S形函數與S形函數主要區別在於函數的值域,雙極S形函數值域是(-1,1),而S形函數值域是(0,1)。
因爲S形函數與雙極S形函數都是可導的(導函數是連續函數),所以適合用在BP神經網絡中。(BP算法要求激活函數可導)
3. 神經網絡模型
神經網絡是由大量的神經元互聯而構成的網絡。根據網絡中神經元的互聯方式,常見網絡結構主要能夠分爲下面3類:
(1) 前饋神經網絡 ( Feedforward Neural Networks )
前饋網絡也稱前向網絡。這種網絡只在訓練過程會有反饋信號,而在分類過程當中數據只能向前傳送,直到到達輸出層,層間沒有向後的反饋信號,所以被稱爲前饋網絡。感知機( perceptron)與BP神經網絡就屬於前饋網絡。
圖4 中是一個3層的前饋神經網絡,其中第一層是輸入單元,第二層稱爲隱含層,第三層稱爲輸出層(輸入單元不是神經元,所以圖中有2層神經元)。

圖4. 前饋神經網絡
對於一個3層的前饋神經網絡N,若用X表示網絡的輸入向量,W1~W3表示網絡各層的鏈接權向量,F1~F3表示神經網絡3層的激活函數。
那麼神經網絡的第一層神經元的輸出爲:
O1 = F1( XW1 )
第二層的輸出爲:
O2 = F2 ( F1( XW1 ) W2 )
輸出層的輸出爲:
O3 = F3( F2 ( F1( XW1 ) W2 ) W3 )
若激活函數F1~F3都選用線性函數,那麼神經網絡的輸出O3將是輸入X的線性函數。所以,若要作高次函數的逼近就應該選用適當的非線性函數做爲激活函數。
(2) 反饋神經網絡 ( Feedback Neural Networks )
反饋型神經網絡是一種從輸出到輸入具備反饋鏈接的神經網絡,其結構比前饋網絡要複雜得多。典型的反饋型神經網絡有:Elman網絡和Hopfield網絡。

圖5. 反饋神經網絡
(3) 自組織網絡 ( SOM ,Self-Organizing Neural Networks )
自組織神經網絡是一種無導師學習網絡。它經過自動尋找樣本中的內在規律和本質屬性,自組織、自適應地改變網絡參數與結構。

圖6. 自組織網絡
4. 神經網絡工做方式
神經網絡運做過程分爲學習和工做兩種狀態。
(1)神經網絡的學習狀態
網絡的學習主要是指使用學習算法來調整神經元間的聯接權,使得網絡輸出更符合實際。學習算法分爲有導師學習( Supervised Learning )與無導師學習( Unsupervised Learning )兩類。
有導師學習算法將一組訓練集 ( training set )送入網絡,根據網絡的實際輸出與指望輸出間的差異來調整鏈接權。有導師學習算法的主要步驟包括:
1) 從樣本集合中取一個樣本(Ai,Bi);
2) 計算網絡的實際輸出O;
3) 求D=Bi-O;
4) 根據D調整權矩陣W;
5) 對每一個樣本重複上述過程,直到對整個樣本集來講,偏差不超過規定範圍。
BP算法就是一種出色的有導師學習算法。
無導師學習抽取樣本集合中蘊含的統計特性,並以神經元之間的聯接權的形式存於網絡中。
Hebb學習律是一種經典的無導師學習算法。
(2) 神經網絡的工做狀態
神經元間的鏈接權不變,神經網絡做爲分類器、預測器等使用。
下面簡要介紹一下Hebb學習率與Delta學習規則 。
(3) 無導師學習算法:Hebb學習率
Hebb算法核心思想是,當兩個神經元同時處於激發狀態時二者間的鏈接權會被增強,不然被減弱。
爲了理解Hebb算法,有必要簡單介紹一下條件反射實驗。巴甫洛夫的條件反射實驗:每次給狗餵食前都先響鈴,時間一長,狗就會將鈴聲和食物聯繫起來。之後若是響鈴可是不給食物,狗也會流口水。

圖7. 巴甫洛夫的條件反射實驗
受該實驗的啓發,Hebb的理論認爲在同一時間被激發的神經元間的聯繫會被強化。好比,鈴聲響時一個神經元被激發,在同一時間食物的出現會激發附近的另 一個神經元,那麼這兩個神經元間的聯繫就會強化,從而記住這兩個事物之間存在着聯繫。相反,若是兩個神經元老是不能同步激發,那麼它們間的聯繫將會愈來愈 弱。
Hebb學習律可表示爲:

其中wij表示神經元j到神經元i的鏈接權,yi與yj爲兩個神經元的輸出,a是表示學習速度的常數。若yi與yj同時被激活,即yi與yj同時爲正,那麼Wij將增大。若yi被激活,而yj處於抑制狀態,即yi爲正yj爲負,那麼Wij將變小。
(4) 有導師學習算法:Delta學習規則
Delta學習規則是一種簡單的有導師學習算法,該算法根據神經元的實際輸出與指望輸出差異來調整鏈接權,其數學表示以下:

其中Wij表示神經元j到神經元i的鏈接權,di是神經元i的指望輸出,yi是神經元i的實際輸出,xj表示神經元j狀態,若神經元j處於激活態則xj爲 1,若處於抑制狀態則xj爲0或-1(根據激活函數而定)。a是表示學習速度的常數。假設xi爲1,若di比yi大,那麼Wij將增大,若di比yi小, 那麼Wij將變小。
Delta規則簡單講來就是:若神經元實際輸出比指望輸出大,則減少全部輸入爲正的鏈接的權重,增大全部輸入爲負的鏈接的權重。反之,若神經元實際輸出比 指望輸出小,則增大全部輸入爲正的鏈接的權重,減少全部輸入爲負的鏈接的權重。這個增大或減少的幅度就根據上面的式子來計算。
(5)有導師學習算法:BP算法
採用BP學習算法的前饋型神經網絡一般被稱爲BP網絡。

圖8. 三層BP神經網絡結構
BP網絡具備很強的非線性映射能力,一個3層BP神經網絡可以實現對任意非線性函數進行逼近(根據Kolrnogorov定理)。一個典型的3層BP神經網絡模型如圖7所示。
BP網絡的學習算法佔篇幅較大,我打算在下一篇文章中介紹。
第二節、神經網絡實現
1. 數據預處理
在訓練神經網絡前通常須要對數據進行預處理,一種重要的預處理手段是歸一化處理。下面簡要介紹歸一化處理的原理與方法。
(1) 什麼是歸一化?
數據歸一化,就是將數據映射到[0,1]或[-1,1]區間或更小的區間,好比(0.1,0.9) 。
(2) 爲何要歸一化處理?
<1>輸入數據的單位不同,有些數據的範圍可能特別大,致使的結果是神經網絡收斂慢、訓練時間長。
<2>數據範圍大的輸入在模式分類中的做用可能會偏大,而數據範圍小的輸入做用就可能會偏小。
<3> 因爲神經網絡輸出層的激活函數的值域是有限制的,所以須要將網絡訓練的目標數據映射到激活函數的值域。例如神經網絡的輸出層若採用S形激活函數,因爲S形 函數的值域限制在(0,1),也就是說神經網絡的輸出只能限制在(0,1),因此訓練數據的輸出就要歸一化到[0,1]區間。
<4>S形激活函數在(0,1)區間之外區域很平緩,區分度過小。例如S形函數f(X)在參數a=1時,f(100)與f(5)只相差0.0067。
(3) 歸一化算法
一種簡單而快速的歸一化算法是線性轉換算法。線性轉換算法常見有兩種形式:
<1>
y = ( x - min )/( max - min )
其中min爲x的最小值,max爲x的最大值,輸入向量爲x,歸一化後的輸出向量爲y 。上式將數據歸一化到 [ 0 , 1 ]區間,當激活函數採用S形函數時(值域爲(0,1))時這條式子適用。
<2>
y = 2 * ( x - min ) / ( max - min ) - 1
這條公式將數據歸一化到 [ -1 , 1 ] 區間。當激活函數採用雙極S形函數(值域爲(-1,1))時這條式子適用。
(4) Matlab數據歸一化處理函數
Matlab中歸一化處理數據能夠採用premnmx , postmnmx , tramnmx 這3個函數。
<1> premnmx
語法:[pn,minp,maxp,tn,mint,maxt] = premnmx(p,t)
參數:
pn: p矩陣按行歸一化後的矩陣
minp,maxp:p矩陣每一行的最小值,最大值
tn:t矩陣按行歸一化後的矩陣
mint,maxt:t矩陣每一行的最小值,最大值
做用:將矩陣p,t歸一化到[-1,1] ,主要用於歸一化處理訓練數據集。
<2> tramnmx
語法:[pn] = tramnmx(p,minp,maxp)
參數:
minp,maxp:premnmx函數計算的矩陣的最小,最大值
pn:歸一化後的矩陣
做用:主要用於歸一化處理待分類的輸入數據。
<3> postmnmx
語法: [p,t] =postmnmx(pn,minp,maxp,tn,mint,maxt)
參數:
minp,maxp:premnmx函數計算的p矩陣每行的最小值,最大值
mint,maxt:premnmx函數計算的t矩陣每行的最小值,最大值
做用:將矩陣pn,tn映射回歸一化處理前的範圍。postmnmx函數主要用於將神經網絡的輸出結果映射回歸一化前的數據範圍。
2. 使用Matlab實現神經網絡
使用Matlab創建前饋神經網絡主要會使用到下面3個函數:
newff :前饋網絡建立函數
train:訓練一個神經網絡
sim :使用網絡進行仿真
下面簡要介紹這3個函數的用法。
(1) newff函數
<1>newff函數語法
newff函數參數列表有不少的可選參數,具體能夠參考Matlab的幫助文檔,這裏介紹newff函數的一種簡單的形式。
語法:net = newff ( A, B, {C} ,‘trainFun’)
參數:
A:一個n×2的矩陣,第i行元素爲輸入信號xi的最小值和最大值;
B:一個k維行向量,其元素爲網絡中各層節點數;
C:一個k維字符串行向量,每一份量爲對應層神經元的激活函數;
trainFun :爲學習規則採用的訓練算法。
<2>經常使用的激活函數
經常使用的激活函數有:
a) 線性函數 (Linear transfer function)
f(x) = x
該函數的字符串爲’purelin’。
b) 對數S形轉移函數( Logarithmic sigmoid transfer function )

該函數的字符串爲’logsig’。
c) 雙曲正切S形函數 (Hyperbolic tangent sigmoid transfer function )

也就是上面所提到的雙極S形函數。
該函數的字符串爲’ tansig’。
Matlab的安裝目錄下的toolbox\nnet\nnet\nntransfer子目錄中有全部激活函數的定義說明。
<3>常見的訓練函數
常見的訓練函數有:
traingd :梯度降低BP訓練函數(Gradient descentbackpropagation)
traingdx :梯度降低自適應學習率訓練函數
<4>網絡配置參數
一些重要的網絡配置參數以下:
net.trainparam.goal :神經網絡訓練的目標偏差
net.trainparam.show : 顯示中間結果的週期
net.trainparam.epochs :最大迭代次數
net.trainParam.lr : 學習率
(2) train函數
網絡訓練學習函數。
語法:[ net, tr, Y1, E ] = train( net, X, Y )
參數:
X:網絡實際輸入
Y:網絡應有輸出
tr:訓練跟蹤信息
Y1:網絡實際輸出
E:偏差矩陣
(3) sim函數
語法:Y=sim(net,X)
參數:
net:網絡
X:輸入給網絡的K×N矩陣,其中K爲網絡輸入個數,N爲數據樣本數
Y:輸出矩陣Q×N,其中Q爲網絡輸出個數
(4) Matlab BP網絡實例
我將Iris數據集分爲2組,每組各75個樣本,每組中每種花各有25個樣本。其中一組做爲以上程序的訓練樣本,另一組做爲檢驗樣本。爲了方便訓練,將3類花分別編號爲1,2,3 。
使用這些數據訓練一個4輸入(分別對應4個特徵),3輸出(分別對應該樣本屬於某一品種的可能性大小)的前向網絡。
Matlab程序以下:
%
讀取訓練數據 [f1,f2,f3,f4,class]
=
textread(
'
trainData.txt
'
,
'
%f%f%f%f%f
'
,
150
);
%
特徵值歸一化 [input,minI,maxI]
=
premnmx( [f1 , f2 , f3 , f4 ]
'
) ;
%
構造輸出矩陣 s
=
length( class
) ; output
=
zeros( s ,
3
) ;
for
i
=
1
: s output( i , class( i ) )
=
1
; end
%
建立神經網絡 net
=
newff( minmax(input) , [
10
3
] , {
'
logsig
'
'
purelin
'
} ,
'
traingdx
'
) ;
%
設置訓練參數 net.trainparam.show
=
50
; net.trainparam.epochs
=
500
; net.trainparam.goal
=
0.01
; net.trainParam.lr
=
0.01
;
%
開始訓練 net
=
train( net, input , output
'
) ;
%
讀取測試數據 [t1 t2 t3 t4 c]
=
textread(
'
testData.txt
'
,
'
%f%f%f%f%f
'
,
150
);
%
測試數據歸一化 testInput
=
tramnmx ( [t1,t2,t3,t4]
'
, minI, maxI ) ;
%
仿真 Y
=
sim( net , testInput )
%
統計識別正確率 [s1 , s2]
=
size( Y ) ; hitNum
=
0 ;
for
i
=
1
: s2 [m , Index]
=
max( Y( : , i ) ) ;
if
( Index
==
c(i) ) hitNum
=
hitNum
+
1
; end end sprintf(
'
識別率是 %3.3f%%
'
,
100
*
hitNum
/
s2 )
以上程序的識別率穩定在95%左右,訓練100次左右達到收斂,訓練曲線以下圖所示:

圖9. 訓練性能表現
(5)參數設置對神經網絡性能的影響
我在實驗中經過調整隱含層節點數,選擇不經過的激活函數,設定不一樣的學習率,
<1>隱含層節點個數
隱含層節點的個數對於識別率的影響並不大,可是節點個數過多會增長運算量,使得訓練較慢。
<2>激活函數的選擇
激活函數不管對於識別率或收斂速度都有顯著的影響。在逼近高次曲線時,S形函數精度比線性函數要高得多,但計算量也要大得多。
<3>學習率的選擇
學習率影響着網絡收斂的速度,以及網絡可否收斂。學習率設置偏小能夠保證網絡收斂,可是收斂較慢。相反,學習率設置偏大則有可能使網絡訓練不收斂,影響識別效果。
3. 使用AForge.NET實現神經網絡
(1) AForge.NET簡介
AForge.NET是一個C#實現的面向人工智能、計算機視覺等領域的開源架構。AForge.NET源代碼下的Neuro目錄包含一個神經網絡的類庫。
AForge.NET主頁:http://www.aforgenet.com/
AForge.NET代碼下載:http://code.google.com/p/aforge/
Aforge.Neuro工程的類圖以下:

圖10. AForge.Neuro類庫類圖
下面介紹圖9中的幾個基本的類:
Neuron —神經元的抽象基類
Layer — 層的抽象基類,由多個神經元組成
Network —神經網絡的抽象基類,由多個層(Layer)組成
IActivationFunction - 激活函數(activation function)的接口
IUnsupervisedLearning - 無導師學習(unsupervised learning)算法的接口ISupervisedLearning - 有導師學習(supervised learning)算法的接口
(2)使用Aforge創建BP神經網絡
使用AForge創建BP神經網絡會用到下面的幾個類:
<1> SigmoidFunction : S形神經網絡
構造函數:public SigmoidFunction( doublealpha )
參數alpha決定S形函數的陡峭程度。
<2> ActivationNetwork :神經網絡類
構造函數:
public ActivationNetwork( IActivationFunction function, int inputsCount, paramsint[] neuronsCount )
: base(inputsCount, neuronsCount.Length )
public virtual double[] Compute( double[]input )
參數意義:
inputsCount:輸入個數
neuronsCount :表示各層神經元個數
<3> BackPropagationLearning:BP學習算法
構造函數:
public BackPropagationLearning( ActivationNetwork network )
參數意義:
network :要訓練的神經網絡對象
BackPropagationLearning類須要用戶設置的屬性有下面2個:
learningRate :學習率
momentum :衝量因子
下面給出一個用AForge構建BP網絡的代碼。
// 建立一個多層神經網絡,採用S形激活函數,各層分別有4,5,3個神經元
//(其中4是輸入個數,3是輸出個數,5是中間層結點個數)ActivationNetwork network =new ActivationNetwork( new SigmoidFunction(2), 4, 5, 3); // 建立訓練算法對象BackPropagationLearning teacher =new BackPropagationLearning(network); // 設置BP算法的學習率與衝量係數teacher.LearningRate =0.1; teacher.Momentum =0; int iteration =1 ; // 迭代訓練500次while( iteration <500 ) { teacher.RunEpoch( trainInput , trainOutput ) ; ++iteration ; } //使用訓練出來的神經網絡來分類,t爲輸入數據向量network.Compute(t)[0]
改程序對Iris 數據進行分類,識別率可達97%左右 。
點擊下載源代碼
文章來自:http://www.cnblogs.com/heaad/