《AI for Game Developers》14 神經(jīng)網(wǎng)絡(luò)篇機(jī)翻
感謝 @神經(jīng)小番茄?
分享的 關(guān)于游戲中人工智能的視頻,并且在視頻下方提供下載資料鏈接。
經(jīng)過幾天的學(xué)習(xí),我已經(jīng)通過機(jī)翻翻譯了其中神經(jīng)網(wǎng)絡(luò)部分文章,并記錄下面,相關(guān)圖片與更多篇章,還需要回到神經(jīng)小番茄視頻中下載
神經(jīng)網(wǎng)絡(luò)
我們的大腦由數(shù)十億個(gè)神經(jīng)元組成,每個(gè)神經(jīng)元都與數(shù)千個(gè)其他神經(jīng)元相連,形成一個(gè)具有非凡處理能力的復(fù)雜網(wǎng)絡(luò)。人工神經(jīng)網(wǎng)絡(luò),以下簡稱神經(jīng)網(wǎng)絡(luò)或網(wǎng)絡(luò),試圖模仿我們大腦的處理能力,盡管規(guī)模要小得多??梢哉f,信息通過軸突和樹突從一個(gè)神經(jīng)元傳遞到另一個(gè)神經(jīng)元。軸突攜帶電壓電位或動(dòng)作電位,從激活的神經(jīng)元到其他連接的神經(jīng)元。動(dòng)作電位是從樹突中的受體獲取的。突觸間隙是發(fā)生化學(xué)反應(yīng)的地方,可以激發(fā)或抑制輸入給定神經(jīng)元的動(dòng)作電位。圖 14-1 說明了一個(gè)神經(jīng)元

成人大腦包含大約 10^11 個(gè)神經(jīng)元,每個(gè)神經(jīng)元接收來自大約 10^4 個(gè)其他神經(jīng)元的突觸輸入。如果所有這些輸入的綜合效果足夠強(qiáng),神經(jīng)元就會(huì)放電,將其動(dòng)作電位傳遞給其他神經(jīng)元。相比之下,我們在游戲中使用的人工網(wǎng)絡(luò)非常簡單。對(duì)于許多應(yīng)用,人工神經(jīng)網(wǎng)絡(luò)僅由少數(shù)幾個(gè)、十幾個(gè)左右的神經(jīng)元組成。這比我們的大腦要簡單得多。一些特定的應(yīng)用程序使用可能由數(shù)千個(gè)神經(jīng)元組成的網(wǎng)絡(luò),但與我們的大腦相比,即使這些神經(jīng)元也很簡單。這個(gè)時(shí)候我們不能指望用我們的人工網(wǎng)絡(luò)來接近人腦的處理能力;然而,對(duì)于特定問題,我們的簡單網(wǎng)絡(luò)可能非常強(qiáng)大。
這是神經(jīng)網(wǎng)絡(luò)的生物學(xué)隱喻。有時(shí)從不太生物學(xué)的角度來思考神經(jīng)網(wǎng)絡(luò)會(huì)很有幫助。具體來說,您可以將神經(jīng)網(wǎng)絡(luò)視為數(shù)學(xué)【函數(shù)逼近器】。網(wǎng)絡(luò)的輸入代表自變量,而輸出代表因變量。網(wǎng)絡(luò)本身是一個(gè)函數(shù),為給定的輸入提供一組獨(dú)特的輸出。這種情況下的函數(shù)很難寫成方程式,幸運(yùn)的是我們不需要這樣做。此外,該函數(shù)是高度非線性的。我們稍后會(huì)回到這種思維方式.
對(duì)于游戲,神經(jīng)網(wǎng)絡(luò)與更傳統(tǒng)的 AI 技術(shù)相比具有一些關(guān)鍵優(yōu)勢。首先,使用神經(jīng)網(wǎng)絡(luò)使游戲開發(fā)人員能夠通過將關(guān)鍵決策過程委托給一個(gè)或多個(gè)訓(xùn)練有素的神經(jīng)網(wǎng)絡(luò)來簡化復(fù)雜狀態(tài)機(jī)或基于規(guī)則的系統(tǒng)的編碼。其次,神經(jīng)網(wǎng)絡(luò)為游戲的人工智能提供了在玩游戲時(shí)進(jìn)行調(diào)整的潛力。這是一個(gè)相當(dāng)引人注目的可能性,并且在撰寫本文時(shí)是游戲 AI 社區(qū)中非常流行的主題.
盡管有這些優(yōu)勢,神經(jīng)網(wǎng)絡(luò)還沒有在視頻游戲中得到廣泛使用。游戲開發(fā)者已經(jīng)在一些流行的游戲中使用了神經(jīng)網(wǎng)絡(luò);但總的來說,它們在游戲中的使用是有限的。這可能是由于幾個(gè)因素造成的,接下來我們將描述其中兩個(gè)關(guān)鍵因素.
首先,神經(jīng)網(wǎng)絡(luò)擅長處理高度非線性的問題;使用傳統(tǒng)方法無法輕松解決的問題。這有時(shí)會(huì)使準(zhǔn)確理解網(wǎng)絡(luò)在做什么以及它如何得出結(jié)果變得難以理解,這可能會(huì)讓未來的測試人員感到不安。其次,有時(shí)很難預(yù)測神經(jīng)網(wǎng)絡(luò)將生成什么作為輸出,尤其是當(dāng)網(wǎng)絡(luò)被編程為在游戲中學(xué)習(xí)或適應(yīng)時(shí)。例如,與測試和調(diào)試有限狀態(tài)機(jī)相比,這兩個(gè)因素使得測試和調(diào)試神經(jīng)網(wǎng)絡(luò)相對(duì)困難。
此外,一些在游戲中使用神經(jīng)網(wǎng)絡(luò)的早期嘗試試圖解決完整的AI系統(tǒng),即組裝大量神經(jīng)網(wǎng)絡(luò)來處理給定游戲生物或角色可能遇到的最一般的 AI 任務(wù)。神經(jīng)網(wǎng)絡(luò)充當(dāng)了整個(gè)人工智能系統(tǒng),可以說是整個(gè)大腦。我們不提倡這種方法,因?yàn)樗觿×伺c可預(yù)測性、測試和調(diào)試相關(guān)的問題。相反,就像我們自己的大腦有許多專門處理特定任務(wù)的區(qū)域一樣,我們建議您使用神經(jīng)網(wǎng)絡(luò)來處理特定的游戲 AI 任務(wù),作為也使用傳統(tǒng) AI 技術(shù)的集成 AI 系統(tǒng)的一部分。通過這種方式,大多數(shù) AI系統(tǒng)將是相對(duì)可預(yù)測的,而困難的AI任務(wù)或您想要利用學(xué)習(xí)和適應(yīng)的任務(wù)將使用針對(duì)該任務(wù)嚴(yán)格訓(xùn)練的特定神經(jīng)網(wǎng)絡(luò)。
AI 社區(qū)使用許多不同種類的神經(jīng)網(wǎng)絡(luò)來解決各種問題,從財(cái)務(wù)到工程問題以及介于兩者之間的許多問題。 神經(jīng)網(wǎng)絡(luò)通常與其他技術(shù),例如模糊系統(tǒng)、遺傳算法和概率方法,僅舉幾例。這個(gè)主題太大了,無法在一章中討論,所以我們將把注意力集中在一個(gè)特別有用的一類神經(jīng)網(wǎng)絡(luò)。 我們將把注意力集中在一種神經(jīng)網(wǎng)絡(luò)上網(wǎng)絡(luò)稱為多層前饋網(wǎng)絡(luò)。 這種類型的網(wǎng)絡(luò)非常通用,并且是能夠處理各種各樣的問題。 在深入了解此類網(wǎng)絡(luò)的細(xì)節(jié)之前,讓我們
首先一般性地探討如何在游戲中應(yīng)用神經(jīng)網(wǎng)絡(luò)。
14.0
14.2.1 Control 控制器
神經(jīng)網(wǎng)絡(luò)通常用作機(jī)器人應(yīng)用的神經(jīng)控制器。在這些情況下,機(jī)器人的感覺系統(tǒng)向神經(jīng)控制器提供相關(guān)輸入,神經(jīng)控制器的輸出(可以由一個(gè)或多個(gè)輸出節(jié)點(diǎn)組成)向機(jī)器人的電機(jī)控制系統(tǒng)發(fā)送適當(dāng)?shù)捻憫?yīng)。例如,機(jī)器人坦克的神經(jīng)控制器可能需要三個(gè)輸入,每個(gè)輸入指示是否在機(jī)器人前方或兩側(cè)感知到障礙物。 (也可以輸入到每個(gè)感測到的障礙物的范圍。)神經(jīng)控制器可以有兩個(gè)輸出來控制其左右軌道的運(yùn)動(dòng)方向。一個(gè)輸出節(jié)點(diǎn)可以設(shè)置左軌道向前或向后移動(dòng),而另一個(gè)可以設(shè)置右軌道向前或向后移動(dòng)。結(jié)果輸出的組合使機(jī)器人向前移動(dòng)、向后移動(dòng)、向左轉(zhuǎn)或向右轉(zhuǎn)。神經(jīng)網(wǎng)絡(luò)可能看起來像圖中所示的那樣?

游戲中也會(huì)出現(xiàn)非常相似的情況。事實(shí)上,您可以在游戲中使用計(jì)算機(jī)控制的半履帶機(jī)械化單元?;蛘吣憧赡芟胧褂蒙窠?jīng)網(wǎng)絡(luò)來處理宇宙飛船或飛機(jī)的飛行控制。在每一種情況下,您都會(huì)有一個(gè)或多個(gè)輸入神經(jīng)元和一個(gè)或多個(gè)輸出神經(jīng)元,它們將控制裝置的推力、輪子、軌道或您正在模擬的任何運(yùn)動(dòng)方式
14.2.2 Threat Assessment (威脅評(píng)估)
技術(shù)并訓(xùn)練部隊(duì)抵御或攻擊計(jì)算機(jī)控制的基地。假設(shè)您決定使用神經(jīng)網(wǎng)絡(luò)為計(jì)算機(jī)控制的軍隊(duì)提供某種方法來預(yù)測玩家在游戲過程中的任何給定時(shí)間提出的威脅類型。一種可能的神經(jīng)網(wǎng)絡(luò)如圖 14-3 所示。

該網(wǎng)絡(luò)的輸入包括敵方(玩家)地面單位的數(shù)量、敵方空中單位的數(shù)量、地面單位是否在移動(dòng)的指示、空中單位是否在移動(dòng)的指示、到地面單位的范圍,以及到空中單位的范圍。輸出由指示四種可能威脅之一的神經(jīng)元組成,包括空中威脅、地面威脅、空中和地面威脅,或沒有威脅。在玩游戲期間給定適當(dāng)?shù)臄?shù)據(jù)和評(píng)估網(wǎng)絡(luò)性能的方法(我們稍后將討論訓(xùn)練),您可以使用這樣的網(wǎng)絡(luò)來預(yù)測即將發(fā)生的攻擊類型(如果有的話)。一旦評(píng)估了威脅,計(jì)算機(jī)就可以采取適當(dāng)?shù)男袆?dòng)。這可能包括部署地面或空中部隊(duì)、加強(qiáng)防御、讓步兵處于高度戒備狀態(tài),或者在沒有威脅的情況下照常進(jìn)行。
這種方法需要在游戲中訓(xùn)練和驗(yàn)證網(wǎng)絡(luò),但可能會(huì)根據(jù)玩家的游戲風(fēng)格進(jìn)行自我調(diào)整。此外,如果您為此任務(wù)使用基于規(guī)則或有限狀態(tài)機(jī)類型的體系結(jié)構(gòu),則可以減輕找出所有可能的場景和閾值的任務(wù)。
14.2.3 Attack or Flee 攻擊或逃跑
最后一個(gè)例子,假設(shè)你有一個(gè)持久的角色扮演游戲,你決定使用神經(jīng)網(wǎng)絡(luò)來控制游戲中某些生物的行為方式?,F(xiàn)在假設(shè)您要使用神經(jīng)網(wǎng)絡(luò)來處理生物的決策過程,即生物是攻擊、躲避還是徘徊,這取決于敵人(玩家)是否在該生物附近。圖 14-4 顯示了這樣一個(gè)神經(jīng)網(wǎng)絡(luò)的外觀。請注意,您只會(huì)使用此網(wǎng)絡(luò)來決定是攻擊、躲避還是徘徊。您可以使用其他游戲邏輯,例如我們之前討論的追逐和躲避技術(shù),來執(zhí)行所需的動(dòng)作。

在本例中我們有四個(gè)輸入:
做出決定(這表明該生物是成群結(jié)隊(duì)還是單獨(dú)旅行);?
衡量生物生命值或生命值的指標(biāo);?
關(guān)于敵人是否參與的指示
與另一個(gè)生物戰(zhàn)斗;?
最后,到敵人的范圍。
我們可以通過添加更多輸入(例如敵人的類別、敵人是法師還是戰(zhàn)士等)使這個(gè)示例更復(fù)雜一些。這樣的考慮對(duì)于其攻擊策略和防御更適合于一種或另一種類的生物來說很重要。你可以通過“作弊”來確定敵人的類別,或者更好的是,你可以通過使用另一個(gè)神經(jīng)網(wǎng)絡(luò)或貝葉斯分析來預(yù)測敵人的類別,為整個(gè)過程增加一點(diǎn)不確定性。

14.1 Dissecting Neural Networks 剖析神經(jīng)網(wǎng)絡(luò)
在本節(jié)中,我們將剖析一個(gè)三層前饋神經(jīng)網(wǎng)絡(luò),查看其每個(gè)組件以了解它們的作用、它們?yōu)楹沃匾约八鼈內(nèi)绾喂ぷ?。這里的目的是清晰簡潔地揭開神經(jīng)網(wǎng)絡(luò)的神秘面紗。我們將采取一種相當(dāng)實(shí)用的方法來完成這項(xiàng)任務(wù),并將一些更具學(xué)術(shù)性的方面留給有關(guān)該主題的其他書籍。我們將在本章中引用幾本這樣的書
14.2.4 Structure 結(jié)構(gòu)
本章重點(diǎn)介紹三層前饋網(wǎng)絡(luò)。圖 14-5 說明了這種網(wǎng)絡(luò)的基本結(jié)構(gòu)

三層網(wǎng)絡(luò)由一層輸入層、一層隱藏層和一層輸出層組成。每層內(nèi)的神經(jīng)元數(shù)量沒有限制。輸入層的每個(gè)神經(jīng)元都連接到隱藏層中的每個(gè)神經(jīng)元。此外,隱藏層中的每個(gè)神經(jīng)元都連接到輸出層中的每個(gè)神經(jīng)元。此外,除了輸入層之外,每個(gè)神經(jīng)元都有一個(gè)額外的輸入,稱為偏差。【圖14-5】中顯示的數(shù)字用于標(biāo)識(shí)三層中的每個(gè)節(jié)點(diǎn)。稍后我們將在編寫計(jì)算每個(gè)神經(jīng)元值的公式時(shí)使用此編號(hào)系統(tǒng)。
計(jì)算網(wǎng)絡(luò)的輸出值從提供給每個(gè)輸入神經(jīng)元的一些輸入開始。然后這些輸入被加權(quán)并傳遞給隱藏層神經(jīng)元。 這個(gè)過程重復(fù),從隱藏層到輸出層,隱藏層神經(jīng)元的輸出作為輸入到輸出層。這個(gè)從輸入層到隱藏層再到輸出層的過程就是前饋過程。 我們將在以下部分更詳細(xì)地研究此類網(wǎng)絡(luò)的每個(gè)組件
14.2.5 Input 輸入
神經(jīng)網(wǎng)絡(luò)的輸入顯然非常重要;沒有它們,神經(jīng)網(wǎng)絡(luò)就無法處理任何東西。顯然我們需要它們,但是您應(yīng)該選擇什么作為輸入?你需要多少?他們應(yīng)該采取什么形式?
14.2.1 Input: What and How Many??
選擇什么作為輸入的問題是非常特定于問題的。你必須審視你試圖解決的問題,并選擇哪些游戲參數(shù)、數(shù)據(jù)和環(huán)境特征對(duì)手頭的任務(wù)很重要。例如,假設(shè)您正在設(shè)計(jì)一個(gè)神經(jīng)網(wǎng)絡(luò)來對(duì)角色扮演游戲中的玩家角色進(jìn)行分類,以便計(jì)算機(jī)控制的生物可以決定是否要與玩家互動(dòng)。您可能會(huì)考慮的一些輸入包括玩家著裝的一些指示、他拔出的武器(如果有)以及可能的任何目擊動(dòng)作,例如,他是否剛剛施了咒語。
如果您將輸入神經(jīng)元的數(shù)量保持在最低限度,您訓(xùn)練神經(jīng)網(wǎng)絡(luò)的工作(我們將在后面討論)將會(huì)更容易。但是,在某些情況下,要選擇的輸入對(duì)您而言并不總是顯而易見的。在這種情況下,一般規(guī)則是包括您認(rèn)為可能重要的輸入,并讓神經(jīng)網(wǎng)絡(luò)自行挑選出哪些是重要的。神經(jīng)網(wǎng)絡(luò)擅長理清輸入對(duì)所需輸出的相對(duì)重要性。但是請記住,您投入的輸入越多,您為訓(xùn)練網(wǎng)絡(luò)準(zhǔn)備的數(shù)據(jù)就越多,您在游戲中必須進(jìn)行的計(jì)算也就越多。
通常,您可以通過將重要信息組合或轉(zhuǎn)換為其他更緊湊的形式來減少輸入的數(shù)量。舉個(gè)簡單的例子,假設(shè)您正在嘗試使用神經(jīng)網(wǎng)絡(luò)來控制飛船在您的游戲中降落在行星上。航天器的質(zhì)量(可能是可變的)和地球重力加速度顯然是重要因素,您應(yīng)該將其作為輸入提供給神經(jīng)網(wǎng)絡(luò)。事實(shí)上,您可以為每個(gè)參數(shù)創(chuàng)建一個(gè)輸入神經(jīng)元,一個(gè)用于質(zhì)量,另一個(gè)用于重力加速度。然而,這種方法迫使神經(jīng)網(wǎng)絡(luò)在計(jì)算航天器質(zhì)量與重力加速度之間的關(guān)系時(shí)執(zhí)行額外的工作。捕獲這兩個(gè)重要參數(shù)的更好輸入是單個(gè)神經(jīng)元,它將航天器的重量(其質(zhì)量乘以重力加速度的乘積)作為單個(gè)神經(jīng)元的輸入。當(dāng)然,除了這個(gè)之外,還有其他輸入神經(jīng)元,例如,您可能還會(huì)有高度和速度輸入.
14.2.2 Input: What Form?
您可以使用各種形式的數(shù)據(jù)作為神經(jīng)網(wǎng)絡(luò)的輸入。 在游戲中,這種輸入一般分為三種類型:布爾型、枚舉型和連續(xù)型。 神經(jīng)網(wǎng)絡(luò)使用實(shí)數(shù),因此無論您擁有何種類型的數(shù)據(jù),都必須將其轉(zhuǎn)換為合適的實(shí)數(shù)才能用作輸入。
考慮圖14-4中所示的示例。“敵人交戰(zhàn)”輸入顯然是一個(gè)布爾類型,如果敵人交戰(zhàn)則為真,否則為假。但是,我們不能將 true 或 false 傳遞給神經(jīng)網(wǎng)絡(luò)輸入節(jié)點(diǎn)。相反,我們輸入 1.0 表示真,0.0 表示假。

有時(shí)您的輸入數(shù)據(jù)可能會(huì)被枚舉。例如,假設(shè)您有一個(gè)旨在對(duì)敵人進(jìn)行分類的網(wǎng)絡(luò),其中一個(gè)考慮因素是他所使用的武器類型。選擇可能是諸如匕首、私生劍、長劍、武士刀、弩、短弓或長弓之類的東西。順序在這里無關(guān)緊要,我們假設(shè)這些可能性是相互排斥的。通常,您使用所謂的 one-of-n 編碼方法在神經(jīng)網(wǎng)絡(luò)中處理此類數(shù)據(jù)。基本上,您為每種可能性創(chuàng)建一個(gè)輸入,并將輸入值設(shè)置為 1.0 或 0.0,以對(duì)應(yīng)每種特定可能性是否為真。例如,如果敵人揮舞著武士刀,則輸入向量將為 {0, 0, 0, 1, 0, 0, 0},其中為武士刀輸入節(jié)點(diǎn)設(shè)置 1,為所有其他節(jié)點(diǎn)設(shè)置 0可能性。
實(shí)際上,您的數(shù)據(jù)通常是浮點(diǎn)數(shù)或整數(shù)。在任何一種情況下,這種類型的數(shù)據(jù)通??梢圆捎靡恍?shí)際上限和下限之間的任意數(shù)量的值。您只需將這些值直接輸入神經(jīng)網(wǎng)絡(luò)(游戲開發(fā)人員經(jīng)常這樣做)。
但是,這可能會(huì)導(dǎo)致一些問題。如果您的輸入值在數(shù)量級(jí)上變化很大,神經(jīng)網(wǎng)絡(luò)可能會(huì)為較大數(shù)量級(jí)的輸入賦予更多權(quán)重。例如,如果一個(gè)輸入的范圍是 0 到 20,而另一個(gè)輸入的范圍是 0 到 20,000,則后者可能會(huì)抵消前者的影響。因此,在這些情況下,重要的是將此類輸入數(shù)據(jù)縮放到在數(shù)量級(jí)方面具有可比性的范圍。通常,您可以根據(jù)從 0 到 100 的百分比值或從 0 到 1 的值來縮放此類數(shù)據(jù)。以這種方式縮放可以為各種輸入提供公平的競爭環(huán)境。但是,您必須小心縮放比例。您需要確保用于訓(xùn)練網(wǎng)絡(luò)的數(shù)據(jù)的縮放方式與網(wǎng)絡(luò)在現(xiàn)場看到的數(shù)據(jù)的縮放方式完全相同。例如,如果您按訓(xùn)練數(shù)據(jù)的屏幕寬度縮放距離輸入值,則當(dāng)網(wǎng)絡(luò)在您的游戲中運(yùn)行時(shí),您必須使用相同的屏幕寬度來縮放輸入數(shù)據(jù)。
14.2.6 Weights
神經(jīng)網(wǎng)絡(luò)中的權(quán)重類似于生物神經(jīng)網(wǎng)絡(luò)中的突觸連接。權(quán)重會(huì)影響給定輸入的強(qiáng)度,并且可以是抑制性的或興奮性的。真正定義神經(jīng)網(wǎng)絡(luò)行為的是權(quán)重。此外,確定這些權(quán)重值的任務(wù)是訓(xùn)練或進(jìn)化神經(jīng)網(wǎng)絡(luò)的主題.
從一個(gè)神經(jīng)元到另一個(gè)神經(jīng)元的每個(gè)連接都有一個(gè)相關(guān)的權(quán)重。如圖14-5所示。神經(jīng)元的輸入是連接該神經(jīng)元的每個(gè)輸入權(quán)重的乘積之和乘以它的輸入值加上偏置項(xiàng),我們將在后面討論。最終結(jié)果稱為神經(jīng)元的凈輸入。以下等式顯示了如何從一組輸入 i 神經(jīng)元計(jì)算給定神經(jīng)元神經(jīng)元 j 的凈輸入.

參考圖 14-5,您可以看到神經(jīng)元的每個(gè)輸入都乘以這兩個(gè)神經(jīng)元之間的連接權(quán)重加上偏差。讓我們看一個(gè)簡單的例子(稍后我們將看一下這些計(jì)算的源代碼).
假設(shè)我們要計(jì)算圖 14-5 所示隱藏層中第 0 個(gè)神經(jīng)元的凈輸入。應(yīng)用前面的等式,我們得到隱藏層中第 0 個(gè)神經(jīng)元的凈輸入的以下公式:

在這個(gè)公式中,ns 代表神經(jīng)元的值。對(duì)于輸入神經(jīng)元,這些是輸入值。在隱藏神經(jīng)元的情況下,它們是凈輸入值。上標(biāo) h 和 i 表示神經(jīng)元屬于哪一層隱藏層和 i 表示輸入層。下標(biāo)表示每一層內(nèi)的節(jié)點(diǎn).
請注意,給定神經(jīng)元的凈輸入只是其他神經(jīng)元加權(quán)輸入的線性組合。如果是這樣,神經(jīng)網(wǎng)絡(luò)如何逼近我們之前提到的高度非線性函數(shù)?關(guān)鍵在于凈輸入如何轉(zhuǎn)換為神經(jīng)元的輸出值。具體來說,激活函數(shù)以非線性方式將凈輸入映射到相應(yīng)的輸出
14.2.7 Activation Functions
激活函數(shù)獲取神經(jīng)元的凈輸入并對(duì)其進(jìn)行操作以產(chǎn)生神經(jīng)元的輸出。激活函數(shù)應(yīng)該是非線性的(除了一種情況,我們將在稍后討論)。如果不是,則神經(jīng)網(wǎng)絡(luò)將簡化為線性函數(shù)的線性組合,并且無法逼近非線性函數(shù)和關(guān)系.
最常用的激活函數(shù)是 logistic 函數(shù)或 sigmoid 函數(shù)。圖 14-6 說明了這個(gè) S 形函數(shù)。

邏輯函數(shù)的公式如下:

有時(shí)這個(gè)函數(shù)寫成 thes 形式:

在這種情況下,c 項(xiàng)用于改變函數(shù)的形狀,即沿水平軸拉伸或壓縮函數(shù).
請注意,輸入位于水平軸上,對(duì)于 x 的所有值,此函數(shù)的輸出范圍從 0 到 1。實(shí)際上,工作范圍更像是 0.1 到 0.9,其中 0.1 左右的值表示神經(jīng)元未激活,而 0.9 左右的值表示神經(jīng)元已激活。重要的是要注意,無論 x 變得多大(正或負(fù)),邏輯函數(shù)實(shí)際上永遠(yuǎn)不會(huì)達(dá)到 1.0 或 0.0;它漸近于這些值。訓(xùn)練時(shí)必須牢記這一點(diǎn)。如果您嘗試訓(xùn)練您的網(wǎng)絡(luò),使其為給定的輸出神經(jīng)元輸出值 1,您將永遠(yuǎn)無法達(dá)到目標(biāo)。一個(gè)更合理的值是 0.9,達(dá)到這個(gè)值將極大地加快訓(xùn)練速度。如果您嘗試訓(xùn)練網(wǎng)絡(luò)輸出值 0,這同樣適用。請改用諸如 0.1 之類的值.
您也可以使用其他激活函數(shù)。圖 14-7 和 14-8 顯示了另外兩個(gè)眾所周知的激活函數(shù):階躍函數(shù)和雙曲正切函數(shù)。


階躍函數(shù)的公式如下:

階躍函數(shù)用于早期的神經(jīng)網(wǎng)絡(luò)開發(fā),但它們?nèi)狈?dǎo)數(shù)使得訓(xùn)練變得困難。請注意,邏輯函數(shù)有一個(gè)易于評(píng)估的導(dǎo)數(shù),這是訓(xùn)練網(wǎng)絡(luò)所必需的,我們很快就會(huì)看到。
雙曲正切函數(shù)的公式如下:

有時(shí)會(huì)使用雙曲正切函數(shù),據(jù)說可以加快訓(xùn)練速度。其他激活函數(shù)在神經(jīng)網(wǎng)絡(luò)中用于各種應(yīng)用;但是,我們不會(huì)在這里討論這些。一般來說,邏輯函數(shù)似乎是使用最廣泛的,適用于各種各樣的應(yīng)用.
Figure 14-9 顯示了有時(shí)使用的另一個(gè)激活函數(shù)——線性激活函數(shù)。

線性激活函數(shù)的公式很簡單:

這意味著神經(jīng)元的輸出只是凈輸入,即所有連接的輸入神經(jīng)元的加權(quán)輸入加上偏置項(xiàng)的總和.
線性激活函數(shù)有時(shí)用作輸出神經(jīng)元的激活函數(shù)。請注意,如果不將網(wǎng)絡(luò)簡化為線性函數(shù)的線性組合,則必須對(duì)隱藏神經(jīng)元使用非線性激活函數(shù)。當(dāng)您不希望輸出限制在 0 和 1 之間的區(qū)間時(shí),使用這樣的線性輸出神經(jīng)元有時(shí)很有用。在這種情況下,您仍然可以使用邏輯輸出激活函數(shù),只要您將輸出縮放到最大您感興趣的值范圍.
14.2.8 Bias
當(dāng)我們之前討論如何計(jì)算神經(jīng)元的凈輸入時(shí),我們提到每個(gè)神經(jīng)元都有一個(gè)與之相關(guān)的偏差。這表示為每個(gè)神經(jīng)元的偏差值和偏差權(quán)重,并顯示在我們之前顯示的凈輸入公式中,為方便起見再次顯示:

b j 是偏置值,wj 是偏置權(quán)重。
要了解偏差的作用,您必須查看用于在給定凈輸入的情況下為神經(jīng)元生成輸出的激活函數(shù)。基本上,偏置項(xiàng)沿激活函數(shù)的水平軸移動(dòng)凈輸入,這有效地改變了神經(jīng)元激活的閾值。偏差值始終設(shè)置為1或-1,其權(quán)重通過訓(xùn)練進(jìn)行調(diào)整,就像所有其他權(quán)重一樣。這實(shí)質(zhì)上允許神經(jīng)網(wǎng)絡(luò)為每個(gè)神經(jīng)元的激活學(xué)習(xí)適當(dāng)?shù)拈撝?
一些從業(yè)者總是將偏差值設(shè)置為 1,而其他人則總是使用 -1。根據(jù)我們的經(jīng)驗(yàn),使用 1 還是 -1 并不重要,因?yàn)橥ㄟ^訓(xùn)練,網(wǎng)絡(luò)會(huì)調(diào)整其偏差權(quán)重以適應(yīng)您的選擇。權(quán)重可以是正的也可以是負(fù)的,所以如果神經(jīng)網(wǎng)絡(luò)認(rèn)為偏差應(yīng)該是負(fù)的,它會(huì)調(diào)整權(quán)重來實(shí)現(xiàn)這一點(diǎn),不管你選擇 1 還是 - 1。如果你選擇 1,它會(huì)找到一個(gè)合適的負(fù)值weight,而如果你選擇-1,它會(huì)找到一個(gè)合適的正權(quán)重。當(dāng)然,您可以通過訓(xùn)練或改進(jìn)網(wǎng)絡(luò)來實(shí)現(xiàn)所有這些,我們將在本章后面討論。
14.2.9 Output
就像輸入一樣,您為給定網(wǎng)絡(luò)選擇的輸出神經(jīng)元是特定于問題的。一般來說,最好將輸出神經(jīng)元的數(shù)量保持在最低限度,以減少計(jì)算和訓(xùn)練時(shí)間.
考慮一個(gè)網(wǎng)絡(luò),其中給定特定輸入,您希望輸出對(duì)該輸入進(jìn)行分類。也許您想確定一組給定的輸入是否屬于某個(gè)類別。在這種情況下,您將使用一個(gè)輸出神經(jīng)元。如果它被激活,結(jié)果為真,而如果它沒有被激活,結(jié)果為假——輸入不屬于所考慮的類別。如果您使用邏輯函數(shù)作為輸出激活,則大約 0.9 的輸出將表示已激活或?yàn)檎?,而大約 0.1 的輸出將表示未激活或?yàn)榧佟T趯?shí)踐中,您實(shí)際上可能無法準(zhǔn)確獲得 0.9 或 0.1 的輸出值;例如,您可能會(huì)得到 0.78 或 0.31。因此,您必須定義一個(gè)閾值,使您能夠評(píng)估給定的輸出值是否表示激活。通常,您只需在兩個(gè)極端之間選擇一個(gè)輸出閾值。對(duì)于邏輯函數(shù),您可以使用 0.5。如果輸出大于0.5,則結(jié)果為激活或?yàn)檎?,否則為假.
當(dāng)您對(duì)某個(gè)輸入是否屬于多個(gè)類別感興趣時(shí),您必須使用多個(gè)輸出神經(jīng)元。 考慮圖 14-3 中所示的網(wǎng)絡(luò)。 在這里,基本上我們想要對(duì)敵人造成的威脅進(jìn)行分類; 這些類別是空中威脅、地面威脅、空中和地面威脅,或沒有威脅。 每個(gè)類別都有一個(gè)輸出神經(jīng)元。 對(duì)于這種類型的輸出,我們假設(shè)高輸出值意味著激活,而低輸出值意味著未激活。每個(gè)節(jié)點(diǎn)的實(shí)際輸出值可以涵蓋一定范圍的值,具體取決于網(wǎng)絡(luò)的訓(xùn)練方式和使用的輸出激活函數(shù)的類型。給定一組輸入值和每個(gè)輸出節(jié)點(diǎn)的結(jié)果值,確定哪個(gè)輸出被激活的一種方法是采用具有最高輸出值的神經(jīng)元。 這就是所謂的贏者通吃的方法。 具有最高激活的神經(jīng)元表示生成的類。 我們將在本章后面看到這種方法的示例。
在其他情況下,如圖 14-2 所示,您可能有多個(gè)輸出神經(jīng)元用于直接控制其他系統(tǒng)。在圖 14-2 所示的示例中,輸出值控制半履帶機(jī)器人每條履帶的運(yùn)動(dòng)。在該示例中,對(duì)輸出神經(jīng)元使用雙曲正切函數(shù)可能很有用,這樣輸出值的范圍將介于 -1 和 +1 之間。然后,負(fù)值可以表示向后運(yùn)動(dòng),而正值可以表示向前運(yùn)動(dòng)。
有時(shí)您可能需要一個(gè)輸出神經(jīng)元與輸入神經(jīng)元一樣多的網(wǎng)絡(luò)。這種網(wǎng)絡(luò)通常用于自動(dòng)關(guān)聯(lián)(模式識(shí)別)和數(shù)據(jù)壓縮。此處的目標(biāo)是輸出神經(jīng)元應(yīng)響應(yīng)輸入值。對(duì)于模式識(shí)別,將訓(xùn)練這樣的網(wǎng)絡(luò)以輸出其輸入。訓(xùn)練集將包含許多感興趣的樣本模式。這里的想法是,當(dāng)呈現(xiàn)出某種程度退化或與訓(xùn)練集中包含的模式不完全匹配的模式時(shí),網(wǎng)絡(luò)應(yīng)該產(chǎn)生代表其訓(xùn)練集中包含的模式的輸出,該模式與正在訓(xùn)練的模式最接近輸入.
14.2.10 The Hidden Layer 隱藏層
到目前為止,我們已經(jīng)討論了輸入神經(jīng)元、輸出神經(jīng)元以及如何計(jì)算任何神經(jīng)元的凈輸入,但我們還沒有專門討論隱藏層。在我們的三層前饋網(wǎng)絡(luò)中,我們有一個(gè)隱藏的神經(jīng)元層夾在輸入層和輸出層之間。

如圖 14-5 所示,每個(gè)輸入都連接到每個(gè)隱藏的神經(jīng)元。此外,每個(gè)隱藏神經(jīng)元將其輸出發(fā)送到每個(gè)輸出神經(jīng)元。順便說一句,這不是您可以使用的唯一神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu);有各種各樣的,有些有不止一個(gè)隱藏層,有些有反饋,有些根本沒有隱藏層,等等。但是,它是最常用的配置之一。無論如何,隱藏層對(duì)于賦予網(wǎng)絡(luò)設(shè)施以處理輸入數(shù)據(jù)中的特征至關(guān)重要。隱藏的神經(jīng)元越多,網(wǎng)絡(luò)可以處理的特征就越多;相反,隱藏神經(jīng)元越少,網(wǎng)絡(luò)可以處理的特征就越少.
那么,我們所說的功能是什么意思?要理解我們在這里的意思,將神經(jīng)網(wǎng)絡(luò)視為函數(shù)逼近器會(huì)很有幫助。假設(shè)你有一個(gè)看起來非常嘈雜的函數(shù),如圖 14-10 所示。

如果你要訓(xùn)練一個(gè)神經(jīng)網(wǎng)絡(luò)來使用太少的隱藏神經(jīng)元來逼近這樣的函數(shù),你可能會(huì)得到如圖 14-11 所示的結(jié)果.

在這里,您可以看到近似函數(shù)捕獲了輸入數(shù)據(jù)的趨勢,但遺漏了局部噪聲特征。在某些情況下,例如對(duì)于信號(hào)降噪應(yīng)用,這正是您想要的;但是,對(duì)于其他問題,您可能不希望這樣做。如果你走另一條路并選擇了太多的隱藏神經(jīng)元,那么除了函數(shù)的整體趨勢之外,近似函數(shù)可能還會(huì)拾取局部噪聲特征。在某些情況下,這可能就是您想要的;然而,在其他情況下,您最終可能會(huì)得到一個(gè)過度訓(xùn)練的網(wǎng)絡(luò),并且無法對(duì)給定的不屬于訓(xùn)練集的新輸入數(shù)據(jù)進(jìn)行泛化
對(duì)于給定的應(yīng)用程序究竟使用多少隱藏神經(jīng)元很難確定。通常,您通過反復(fù)試驗(yàn)來解決它。但是,這里有一條您可能會(huì)覺得有用的經(jīng)驗(yàn)法則。對(duì)于你對(duì)自動(dòng)關(guān)聯(lián)不感興趣的三層網(wǎng)絡(luò),合適的隱藏神經(jīng)元數(shù)量大約等于輸入和輸出神經(jīng)元數(shù)量乘積的平方根。這只是一個(gè)近似值,但它是一個(gè)很好的起點(diǎn)。需要記住的是,尤其是對(duì)于 CPU 使用率至關(guān)重要的游戲,隱藏神經(jīng)元的數(shù)量越多,計(jì)算網(wǎng)絡(luò)輸出所需的時(shí)間就越多。因此,盡量減少隱藏神經(jīng)元的數(shù)量是有益的

14.2 Training 訓(xùn)練
到目前為止,我們已經(jīng)多次提到訓(xùn)練神經(jīng)網(wǎng)絡(luò),但實(shí)際上并未向您提供有關(guān)如何執(zhí)行此操作的詳細(xì)信息。我們將在本節(jié)中解決這個(gè)問題
訓(xùn)練的目的是找到連接所有神經(jīng)元的權(quán)重值,以便輸入數(shù)據(jù)生成所需的輸出值。正如您所料,訓(xùn)練不僅僅是選擇一些權(quán)重值。本質(zhì)上,訓(xùn)練神經(jīng)網(wǎng)絡(luò)是一個(gè)優(yōu)化過程,在這個(gè)過程中,您試圖找到最佳權(quán)重,使網(wǎng)絡(luò)能夠產(chǎn)生正確的輸出
訓(xùn)練可以分為兩類:有監(jiān)督的"訓(xùn)練"和"無監(jiān)督"的訓(xùn)練。涵蓋所有甚至一些流行的訓(xùn)練方法遠(yuǎn)遠(yuǎn)超過一章,因此我們將重點(diǎn)關(guān)注最常用的監(jiān)督訓(xùn)練方法之一:反向傳播
14.2.11 Back-Propagation Training 逆向傳播訓(xùn)練
同樣,“訓(xùn)練的目的”是找到連接所有神經(jīng)元的權(quán)重值,以便輸入數(shù)據(jù)生成所需的輸出值。為此,您需要一個(gè)訓(xùn)練集,其中包含“輸入數(shù)據(jù)”和與該輸入對(duì)應(yīng)的所需輸出值。下一步是迭代地使用多種技術(shù)中的任何一種,為整個(gè)網(wǎng)絡(luò)找到一組權(quán)重,使網(wǎng)絡(luò)產(chǎn)生與訓(xùn)練集中每組數(shù)據(jù)的所需輸出相匹配的輸出。完成此操作后,您就可以讓網(wǎng)絡(luò)開始工作并向其呈現(xiàn)未包含在訓(xùn)練集中的新數(shù)據(jù),以產(chǎn)生合理的輸出
因?yàn)橛?xùn)練是一個(gè)優(yōu)化過程,所以我們需要一些衡量指標(biāo)來優(yōu)化。在反向傳播的情況下,我們使用誤差度量并嘗試最小化誤差。給定一些輸入和生成的輸出,我們需要將生成的輸出與已知的期望輸出進(jìn)行比較,并量化結(jié)果的匹配程度,即計(jì)算誤差。有許多誤差度量可供您使用,我們將在這里使用最常見的一種:均方誤差,它只是計(jì)算輸出與所需輸出之間差異的平方的平均值
如果您學(xué)習(xí)過微積分,您可能會(huì)記得要最小化或最大化函數(shù),您需要能夠計(jì)算函數(shù)的導(dǎo)數(shù)。因?yàn)槲覀冊噲D通過最小化誤差度量來優(yōu)化權(quán)重,所以我們需要在某處計(jì)算導(dǎo)數(shù)也就不足為奇了。具體來說,我們需要激活函數(shù)的導(dǎo)數(shù),這就是邏輯函數(shù)如此好用的原因——我們可以很容易地通過分析確定它的導(dǎo)數(shù)
正如我們之前提到的,找到最佳權(quán)重是一個(gè)迭代過程,它是這樣的:
1. 從一個(gè)由輸入數(shù)據(jù)和相應(yīng)的期望輸出組成的訓(xùn)練集開始。
2. 將神經(jīng)網(wǎng)絡(luò)中的權(quán)重初始化為一些小的隨機(jī)值。
3. 使用每組輸入數(shù)據(jù),饋入網(wǎng)絡(luò)并計(jì)算輸出。
4. 將計(jì)算出的輸出與期望的輸出進(jìn)行比較并計(jì)算誤差。
5. 調(diào)整權(quán)重減少誤差,重復(fù)上述過程
您可以通過兩種方式執(zhí)行該過程。一種方法是計(jì)算誤差度量,調(diào)整每組輸入和所需輸出數(shù)據(jù)的權(quán)重,然后繼續(xù)處理下一組輸入/輸出數(shù)據(jù)。另一種方法是計(jì)算訓(xùn)練集中所有輸入和期望輸出數(shù)據(jù)的累積誤差,然后調(diào)整權(quán)重,然后重復(fù)這個(gè)過程。每次迭代稱為一個(gè)紀(jì)元。
步驟 1 到 3 相對(duì)簡單,稍后我們將看到示例實(shí)現(xiàn)。不過,現(xiàn)在讓我們更仔細(xì)地檢查步驟 4 和 5
14.2.3 Computing Error 計(jì)算誤差
要訓(xùn)練神經(jīng)網(wǎng)絡(luò),您需要為其提供一組輸入,這會(huì)生成一些輸出。要將此計(jì)算出的輸出與給定輸入集的所需輸出進(jìn)行比較,您需要計(jì)算誤差。這使您不僅可以確定計(jì)算出的輸出是對(duì)還是錯(cuò),還可以確定其對(duì)或錯(cuò)的程度。最常使用的誤差是均方誤差,它是期望輸出和計(jì)算輸出之間差值的平方的平均值:

在這個(gè)等式中,是訓(xùn)練集的均方誤差。 nc 和 nd 分別是所有輸出神經(jīng)元的計(jì)算輸出值和期望輸出值,而 m 是每個(gè) epoch 的輸出神經(jīng)元數(shù)。
目標(biāo)是通過迭代調(diào)整連接網(wǎng)絡(luò)中所有神經(jīng)元的權(quán)重值,使這個(gè)誤差值盡可能小。要知道需要調(diào)整多少權(quán)重,每次迭代都需要我們還計(jì)算與輸出層和隱藏層中每個(gè)神經(jīng)元相關(guān)的誤差。我們計(jì)算輸出神經(jīng)元的誤差如下:

這里,io 是第 i 個(gè)輸出神經(jīng)元的誤差,n io 是第 i 個(gè)輸出神經(jīng)元的計(jì)算輸出和期望輸出之間的差異,f'(ncio) 是第 i 個(gè)輸出神經(jīng)元的激活函數(shù)的導(dǎo)數(shù)。早些時(shí)候我們告訴過您我們需要在某處計(jì)算導(dǎo)數(shù),這就是計(jì)算的地方。這就是邏輯函數(shù)如此有用的原因;它的導(dǎo)數(shù)形式很簡單,很容易解析計(jì)算。使用logistic函數(shù)的導(dǎo)數(shù)重寫此等式可得出以下輸出神經(jīng)元誤差等式:

在這個(gè)等式中,n dio 是第 i 個(gè)神經(jīng)元的期望輸出值,ncio 是第 i 個(gè)神經(jīng)元的計(jì)算輸出值。
對(duì)于隱藏層神經(jīng)元,誤差方程有些不同。在這種情況下,與每個(gè)隱藏神經(jīng)元相關(guān)的誤差如下:

請注意,每個(gè)隱藏層神經(jīng)元的誤差是與隱藏神經(jīng)元連接的每個(gè)輸出層神經(jīng)元相關(guān)的誤差乘以每個(gè)連接的權(quán)重的函數(shù)。這意味著要計(jì)算誤差并隨后調(diào)整權(quán)重,您需要從輸出層向輸入層反向工作。
另請注意,再次需要激活函數(shù)導(dǎo)數(shù)。假設(shè)邏輯激活函數(shù)產(chǎn)生以下結(jié)果:

最后,沒有錯(cuò)誤與輸入層神經(jīng)元相關(guān),因?yàn)檫@些神經(jīng)元值是給定的。
14.2.4 Adjusting Weights 調(diào)整權(quán)重
有了計(jì)算出的誤差,您就可以繼續(xù)為網(wǎng)絡(luò)中的每個(gè)權(quán)重計(jì)算適當(dāng)?shù)恼{(diào)整。對(duì)每個(gè)權(quán)重的調(diào)整如下:

在這個(gè)等式中,是學(xué)習(xí)率,i 是與所考慮的神經(jīng)元相關(guān)的誤差,ni 是所考慮的神經(jīng)元的值。新權(quán)重就是舊權(quán)重加上 w。
請記住,每個(gè)重量都會(huì)進(jìn)行重量調(diào)整,并且每個(gè)重量的調(diào)整都會(huì)有所不同。當(dāng)更新連接輸出到隱藏層神經(jīng)元的權(quán)重時(shí),輸出神經(jīng)元的誤差和值計(jì)算權(quán)重調(diào)整。在更新連接隱藏層到輸入層神經(jīng)元的權(quán)重時(shí),使用隱藏層神經(jīng)元的誤差和值。
學(xué)習(xí)率是一個(gè)乘數(shù),會(huì)影響每個(gè)權(quán)重的調(diào)整量。它通常設(shè)置為一些較小的值,例如 0.25或0.5。這是您必須調(diào)整的參數(shù)之一。如果設(shè)置得太高,可能會(huì)超過最佳權(quán)重;如果設(shè)置得太低,訓(xùn)練可能需要更長的時(shí)間。
14.2.5 Momentum 動(dòng)量
許多反向傳播從業(yè)者對(duì)我們剛剛討論的權(quán)重調(diào)整進(jìn)行了細(xì)微的修改。這種改進(jìn)的技術(shù)稱為增加動(dòng)量。在向您展示如何增加動(dòng)力之前,讓我們先討論一下為什么您可能想要增加動(dòng)力。
在任何一般優(yōu)化過程中,目標(biāo)都是最小化或最大化某些函數(shù)。更具體地說,我們有興趣在某些輸入?yún)?shù)范圍內(nèi)找到給定函數(shù)的全局最小值或最大值。問題是許多函數(shù)表現(xiàn)出所謂的局部最小值或最大值。這些基本上是函數(shù)中的凹陷和凸起,如圖 14-12 所示。

在此示例中,該函數(shù)在所示范圍內(nèi)具有全局最小值和最大值;但它也有幾個(gè)局部最小值和最大值,其特征是較小的凸起和凹陷。
在我們的例子中,我們感興趣的是最小化我們網(wǎng)絡(luò)的錯(cuò)誤。具體來說,我們感興趣的是找到產(chǎn)生全局最小誤差的最佳權(quán)重;然而,我們可能會(huì)遇到局部最小值而不是全局最小值。
當(dāng)網(wǎng)絡(luò)訓(xùn)練開始時(shí),我們將權(quán)重初始化為一些小的隨機(jī)值。那時(shí)我們不知道這些值與最佳權(quán)重有多接近;因此,我們可能已將網(wǎng)絡(luò)初始化為接近局部最小值而不是全局最小值。在不涉及微積分的情況下,我們更新權(quán)重的技術(shù)稱為梯度下降類型的技術(shù),我們使用函數(shù)的導(dǎo)數(shù)試圖轉(zhuǎn)向最小值,在我們的例子中是最小誤差值。問題是我們不知道我們是否達(dá)到了全局最小值或局部最小值,通常錯(cuò)誤空間,正如神經(jīng)網(wǎng)絡(luò)所稱的那樣,充滿了局部最小值。
這種問題在所有優(yōu)化技術(shù)中都很常見,許多不同的方法都試圖緩解它。動(dòng)量技術(shù)是一種用于神經(jīng)網(wǎng)絡(luò)的技術(shù)。它并沒有消除收斂于局部最小值的可能性,但它被認(rèn)為有助于擺脫它們并走向全局最小值,這也是它得名的地方?;旧?,我們在權(quán)重調(diào)整中添加了一個(gè)小的附加分?jǐn)?shù),它是前一次迭代權(quán)重調(diào)整的函數(shù)。這會(huì)稍微推動(dòng)權(quán)重調(diào)整,以便在接近局部最小值時(shí),該算法有望超越局部最小值并繼續(xù)朝著全局最小值前進(jìn)。
因此,使用動(dòng)量,計(jì)算權(quán)重調(diào)整的新公式如下:

在這個(gè)等式中,w' 是前一次迭代的權(quán)重調(diào)整,是動(dòng)量因子。 動(dòng)量因子是您必須調(diào)整的另一個(gè)因素。 它通常設(shè)置為 0.0 和 1.0 之間的一些小分?jǐn)?shù)。

14.3 Neural Network Source Code 神經(jīng)網(wǎng)絡(luò)源代碼
最后是時(shí)候查看一些實(shí)現(xiàn)三層前饋神經(jīng)網(wǎng)絡(luò)的實(shí)際源代碼了。以下部分介紹了實(shí)現(xiàn)此類網(wǎng)絡(luò)的兩個(gè) C++ 類。在本章的后面,我們將查看這些類的示例實(shí)現(xiàn)。如果您希望在查看神經(jīng)網(wǎng)絡(luò)的內(nèi)部細(xì)節(jié)之前先了解神經(jīng)網(wǎng)絡(luò)的使用方式,請隨時(shí)跳至標(biāo)題為“用大腦追逐和逃避”的部分。
我們需要在三層前饋神經(jīng)網(wǎng)絡(luò)中實(shí)現(xiàn)兩個(gè)類。第一類代表一個(gè)通用層。您可以將它用于輸入層、隱藏層和輸出層。第二類表示由三層組成的整個(gè)神經(jīng)網(wǎng)絡(luò)。以下部分展示了每個(gè)類的完整源代碼。
14.2.12 The Layer Class
NeuralNetworkLayer 類在多層前饋網(wǎng)絡(luò)中實(shí)現(xiàn)了一個(gè)通用層。它負(fù)責(zé)處理層中包含的神經(jīng)元。它執(zhí)行的任務(wù)包括分配和釋放內(nèi)存以存儲(chǔ)神經(jīng)元值、錯(cuò)誤和權(quán)重;初始化權(quán)重;計(jì)算神經(jīng)元值;和調(diào)整權(quán)重。示例 14-1 顯示了此類的標(biāo)頭。
14-1. NeuralNetworkLayer class
class NeuralNetworkLayer
{
public:
????int NumberOfNodes;
????int NumberOfChildNodes;
????int NumberOfParentNodes;
????double** Weights;
????double** WeightChanges;
????double* NeuronValues;
????double* DesiredValues;
????double* Errors;
????double* BiasWeights;
????double* BiasValues;
????double LearningRate;
????bool LinearOutput;
????bool UseMomentum;
????double MomentumFactor;
????NeuralNetworkLayer* ParentLayer;
????NeuralNetworkLayer* ChildLayer;
????NeuralNetworkLayer();
????void Initialize(int NumNodes, NeuralNetworkLayer* parent, NeuralNetworkLayer* child);
????void CleanUp(void);
????void RandomizeWeights(void);
????void CalculateErrors(void);
????void AdjustWeights(void);
????void CalculateNeuronValues(void);
};
層以父子關(guān)系相互連接。例如,輸入層是隱藏層的父層,隱藏層是輸出層的父層。另外,輸出層是隱藏層的子層,隱藏層是輸入層的子層。注意輸入層沒有父層,輸出層也沒有子層。
此類的成員主要由存儲(chǔ)神經(jīng)元權(quán)重、值、誤差和偏置項(xiàng)的數(shù)組組成。此外,一些成員存儲(chǔ)了一些管理層行為的設(shè)置。成員如下
NumberOfNodes
????該成員存儲(chǔ)層類的給定實(shí)例中神經(jīng)元或節(jié)點(diǎn)的數(shù)量。
NumberOfChildNodes
????該成員存儲(chǔ)連接到層類的給定實(shí)例的子層中的神經(jīng)元數(shù)量。
NumberOfParentNodes
????該成員存儲(chǔ)父層中連接到層類的給定實(shí)例的神經(jīng)元數(shù)量
Weights
????該成員是指向雙精度值的指針?;旧希@表示連接父層和子層之間節(jié)點(diǎn)的二維權(quán)重值數(shù)組。
WeightChanges
????該成員也是一個(gè)指向雙精度值的指針,它訪問一個(gè)動(dòng)態(tài)分配的二維數(shù)組。在這種情況下,存儲(chǔ)在數(shù)組中的值是對(duì)權(quán)重值所做的調(diào)整。正如我們之前討論的那樣,我們需要這些來實(shí)現(xiàn)動(dòng)力
NeuronValues
????該成員是指向雙精度值的指針,它訪問動(dòng)態(tài)分配的數(shù)組,該數(shù)組存儲(chǔ)層中神經(jīng)元的計(jì)算值或激活
DesiredValues
????該成員是指向雙精度值的指針,它訪問動(dòng)態(tài)分配的數(shù)組,該數(shù)組存儲(chǔ)層中神經(jīng)元的所需或目標(biāo)值。我們將其用于輸出數(shù)組,我們在給定計(jì)算輸出和訓(xùn)練集中的目標(biāo)輸出的情況下計(jì)算誤差
Errors
????該成員是一個(gè)指向雙精度值的指針,它訪問一個(gè)動(dòng)態(tài)分配的數(shù)組,該數(shù)組存儲(chǔ)與層中每個(gè)神經(jīng)元相關(guān)的錯(cuò)誤。
BiasWeights
????該成員是一個(gè)指向雙精度值的指針,它訪問一個(gè)動(dòng)態(tài)分配的數(shù)組,該數(shù)組存儲(chǔ)連接到層中每個(gè)神經(jīng)元的偏置權(quán)重。
BiasValues
????該成員是一個(gè)指向雙精度值的指針,它訪問一個(gè)動(dòng)態(tài)分配的數(shù)組,該數(shù)組存儲(chǔ)連接到層中每個(gè)神經(jīng)元的偏置值。 請注意,這個(gè)成員并不是真正需要的,因?yàn)槲覀兺ǔ⑵钪翟O(shè)置為 +1 或 -1 并保持不變
LearningRate
????該成員存儲(chǔ)學(xué)習(xí)率,計(jì)算權(quán)重調(diào)整
LinearOutput
????該成員存儲(chǔ)一個(gè)標(biāo)志,指示是否對(duì)層中的神經(jīng)元使用線性激活函數(shù)。僅當(dāng)圖層是輸出圖層時(shí)才使用它。如果此標(biāo)志為假,則改用邏輯激活函數(shù)。默認(rèn)值為 false
UseMomentum
????該成員存儲(chǔ)一個(gè)標(biāo)志,指示在調(diào)整權(quán)重時(shí)是否使用動(dòng)量。默認(rèn)值為 false
MomentumFactor
????正如我們之前討論的,該成員存儲(chǔ)動(dòng)量因子。僅當(dāng) UseMomentum 標(biāo)志為真時(shí)才使用它。
ParentLayer
????該成員存儲(chǔ)指向NeuralNetworkLayer實(shí)例的指針,該實(shí)例表示連接到給定層實(shí)例的父層。對(duì)于輸入層,此指針設(shè)置為 NULL
ChildLayer
????該成員存儲(chǔ)指向NeuralNetworkLayer實(shí)例的指針,該實(shí)例表示連接到給定層實(shí)例的子層。對(duì)于輸出層,此指針設(shè)置為 NULL。
Example 14-2. NeuralNetworkLayer constructor 構(gòu)造函數(shù)
NeuralNetworkLayer::NeuralNetworkLayer()
{
????ParentLayer = NULL;
????ChildLayer = NULL;
????LinearOutput = false;
????UseMomentum = false;
????MomentumFactor = 0.9;
}
構(gòu)造函數(shù)非常簡單。它所做的只是初始化一些我們已經(jīng)討論過的設(shè)置。示例 14-3 中所示的 Initialize 方法稍微復(fù)雜一些。
Example 14-3. Initialize method 初始化方法
void NeuralNetworkLayer::Initialize(int NumNodes,
????????????????????????????????????????????????????????????????????????????????NeuralNetworkLayer* parent,
????????????????????????????????????????????????????????????????????????????????NeuralNetworkLayer* child)
{
????int i, j;
????// Allocate memory
????NeuronValues = (double*) malloc(sizeof(double) * NumberOfNodes);
????DesiredValues = (double*) malloc(sizeof(double) * NumberOfNodes);
????Errors = (double*) malloc(sizeof(double) * NumberOfNodes);
????if(parent != NULL)
????{
????????ParentLayer = parent;
????}
????if(child != NULL)
????{
????????ChildLayer = child;
????????Weights = (double**) malloc(sizeof(double*) *
????????????????????????????????????????????????????????????????NumberOfNodes);
????????WeightChanges = (double**) malloc(sizeof(double*) *
?????????????????????????????????????????????????????????????????????????????NumberOfNodes);
????????for(i = 0; i<NumberOfNodes; i++)
????????{
????????????Weights[i] = (double*) malloc(sizeof(double) *
????????????????????????????????????????????????????????????????????????NumberOfChildNodes);
????????????WeightChanges[i] = (double*) malloc(sizeof(double) *
????????????????????????????????????????????????????????????????????????????????????NumberOfChildNodes);
????????}
????????BiasValues = (double*) malloc(sizeof(double) * ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ????????????????????????????????? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? NumberOfChildNodes);
? ? ? ? ?BiasWeights = (double*) malloc(sizeof(double) * ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ????????? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? NumberOfChildNodes);
????? ? } else {
????? ? ? ? ?Weights = NULL;
????? ? ? ? ?BiasValues = NULL;
????? ? ? ? ?BiasWeights = NULL;
????? ? ? ? ?WeightChanges = NULL;
????? ? }
????? ? // Make sure everything contains 0s
????? ? for(i=0; i<NumberOfNodes; i++)
????? ? {
????? ? ? ? ?NeuronValues[i] = 0;
????? ? ? ? ?DesiredValues[i] = 0;
????? ? ? ? ?Errors[i] = 0;
????? ? ? ? ?if(ChildLayer != NULL)
????? ? ? ? ? ? ? for(j=0; j<NumberOfChildNodes; j++)
????? ? ? ? ? ? ? {
????? ? ? ? ? ? ? ? ? ?Weights[i][j] = 0;
????? ? ? ? ? ? ? ? ? ?WeightChanges[i][j] = 0;
????? ? ? ? ? ? ? }
????}
????// Initialize the bias values and weights
????if(ChildLayer != NULL)
????????for(j=0; j<NumberOfChildNodes; j++)
????????{
????????????BiasValues[j] = -1;
????????????BiasWeights[j] = 0;
????????}
}
Initialize 方法負(fù)責(zé)為動(dòng)態(tài)數(shù)組分配所有內(nèi)存,用于存儲(chǔ)層中神經(jīng)元的權(quán)重、值、誤差、偏差值和權(quán)重。它還處理初始化所有這些數(shù)組。
該方法采用三個(gè)參數(shù):層中節(jié)點(diǎn)或神經(jīng)元的數(shù)量;指向父層的指針;和指向子層的指針。如果該層為輸入層,父層指針應(yīng)傳入NULL。如果該層是輸出層,則應(yīng)為子層指針傳入 NULL。
進(jìn)入該方法后,會(huì)為NeuronValues、DesiredValues和Errors數(shù)組分配內(nèi)存。所有這些數(shù)組都是一維的,條目數(shù)由層中的節(jié)點(diǎn)數(shù)定義。
接下來,設(shè)置父子層指針。如果子層指針不為 NULL,則我們有一個(gè)輸入層或一個(gè)隱藏層,并且必須為連接權(quán)重分配內(nèi)存。因?yàn)閃eights和WeightChanges是二維數(shù)組,我們需要分步分配內(nèi)存。第一步涉及分配內(nèi)存以保存指向雙精度數(shù)組的指針。這里的條目數(shù)對(duì)應(yīng)于層中的節(jié)點(diǎn)數(shù)。接下來,我們?yōu)槊總€(gè)條目分配另一塊內(nèi)存來存儲(chǔ)實(shí)際的數(shù)組值。這些附加塊的大小對(duì)應(yīng)于子層中的節(jié)點(diǎn)數(shù)。輸入層或隱藏層中的每個(gè)神經(jīng)元都連接到相關(guān)子層中的每個(gè)神經(jīng)元;因此,權(quán)重和權(quán)重調(diào)整數(shù)組的總大小等于該層中的神經(jīng)元數(shù)乘以子層中的神經(jīng)元數(shù)。
我們還繼續(xù)為偏差值和權(quán)重?cái)?shù)組分配內(nèi)存。這些數(shù)組的大小等于連接的子層中神經(jīng)元的數(shù)量。
分配完所有內(nèi)存后,初始化數(shù)組。在大多數(shù)情況下,我們希望所有內(nèi)容都包含0,但偏差值除外,我們將所有偏差值條目設(shè)置為 -1。請注意,您可以將這些全部設(shè)置為 +1,正如我們之前討論的那樣。
Example 14-4 顯示了 CleanUp 方法,它負(fù)責(zé)釋放在 Initialization 方法中分配的所有內(nèi)存。
Example 14-4. CleanUp method 清理方法
void NeuralNetworkLayer::CleanUp(void)
{
????int i;
????free(NeuronValues);
????free(DesiredValues);
????free(Errors);
????if(Weights != NULL)
????{
????????for(i = 0; i<NumberOfNodes; i++)
????????{
????????????free(Weights[i]);
????????????free(WeightChanges[i]);
????????}
????????free(Weights);
????????free(WeightChanges);
????}
????if(BiasValues != NULL) free(BiasValues);
????if(BiasWeights != NULL) free(BiasWeights);
}
這里的代碼是不言自明的。它只是使用 free 釋放所有動(dòng)態(tài)分配的內(nèi)存。
前面我們提到神經(jīng)網(wǎng)絡(luò)權(quán)重在訓(xùn)練開始之前被初始化為一些小的隨機(jī)數(shù)。例 14-5 中所示的 RandomizeWeights 方法為我們處理了這個(gè)任務(wù)。
Example 14-5. RandomizeWeights method 方法
void NeuralNetworkLayer::RandomizeWeights(void)
{
????int i,j;
????int min = 0;
????int max = 200;
????int number;
????srand( (unsigned)time( NULL ) );
????for(i=0; i<NumberOfNodes; i++)
????{
????for(j=0; j<NumberOfChildNodes; j++)
????{
????????number = (((abs(rand())%(max-min+1))+min));
????????if(number>max)????? ?
????????????number = max;
????????if(number<min)
????????????number = min;
????????Weights[i][j] = number / 100.0f - 1;
????}
}
for(j=0; j<NumberOfChildNodes; j++)
{
????????????????number = (((abs(rand())%(max-min+1))+min));
????????????????if(number>max)
????????????????number = max;
????????????????if(number<min)
????????????????number = min;
????????????????BiasWeights[j] = number / 100.0f - 1;
????}
}
此方法所做的只是簡單地為 Weights 數(shù)組中的每個(gè)權(quán)重計(jì)算一個(gè)介于 -1 和 +1 之間的隨機(jī)數(shù)。它對(duì)存儲(chǔ)在 BiasWeights 數(shù)組中的偏置權(quán)重執(zhí)行相同的操作。您應(yīng)該只在訓(xùn)練開始時(shí)調(diào)用此方法。
下一個(gè)方法 CalculateNeuronValues,負(fù)責(zé)使用我們之前向您展示的神經(jīng)元凈輸入公式和激活函數(shù)計(jì)算層中每個(gè)神經(jīng)元的激活值或值。示例 14-6 顯示了此方法。
Example 14-6. CalculateNeuronValues method 計(jì)算神經(jīng)元值方法
void NeuralNetworkLayer::CalculateNeuronValues(void)
{
????int i,j;
????double x;
????if(ParentLayer != NULL)
????{
????????for(j=0; j<NumberOfNodes; j++)
????????{
????????????x = 0;
????????????for(i=0; i<NumberOfParentNodes; i++)
????????????{
????????????x += ParentLayer->NeuronValues[i] *
????????????????ParentLayer->Weights[i][j];
????????????}
????????????x += ParentLayer->BiasValues[j] *
????????????????????ParentLayer->BiasWeights[j];
???? ????????if((ChildLayer == NULL) && LinearOutput)
????????????????NeuronValues[j] = x;
?????????????else
????????????????????NeuronValues[j] = 1.0f/(1+exp(-x));
????????????}
? ????}
}
在此方法中,所有權(quán)重都使用嵌套的 for 語句進(jìn)行循環(huán)。 j 循環(huán)循環(huán)遍歷層節(jié)點(diǎn)(子層),而 i 循環(huán)循環(huán)遍歷父層節(jié)點(diǎn)。在這些嵌套循環(huán)中,計(jì)算凈輸入并將其存儲(chǔ)在x變量中。該層中每個(gè)節(jié)點(diǎn)的凈輸入是來自父層(第 i 個(gè)循環(huán))饋送到每個(gè)節(jié)點(diǎn)(第 j 個(gè)節(jié)點(diǎn))的所有連接的加權(quán)和,加上第 j 個(gè)節(jié)點(diǎn)的加權(quán)偏差。
在計(jì)算出每個(gè)節(jié)點(diǎn)的凈輸入后,您可以通過應(yīng)用激活函數(shù)來計(jì)算每個(gè)神經(jīng)元的值。您對(duì)除輸出層之外的所有層都使用邏輯激活函數(shù),在這種情況下,您根據(jù) LinearOutput 標(biāo)志使用線性激活函數(shù)。
示例 14-7 中所示的 CalculateErrors 方法負(fù)責(zé)使用我們之前討論的公式計(jì)算與每個(gè)神經(jīng)元相關(guān)的誤差。
Example 14-7. CalculateErrors method 方法
void NeuralNetworkLayer::CalculateErrors(void)
{
????int i, j;
????double sum;
????if(ChildLayer == NULL) // output layer
????{
????????for(i=0; i<NumberOfNodes; i++)
????{
????????Errors[i] = (DesiredValues[i] - NeuronValues[i]) *?
????????????????????????????NeuronValues[i] *?
????????????????????????????(1.0f -NeuronValues[i]);
? ????}
????} else if(ParentLayer == NULL) { // input layer for(i=0; i<NumberOfNodes; i++)
????????{
????????????Errors[i] = 0.0f;
????????}
????} else { // hidden layer
????????for(i=0; i<NumberOfNodes; i++)
????????{
????????????sum = 0;
????????????for(j=0; j<NumberOfChildNodes; j++)
????????????{
? ? ???? ????????sum += ChildLayer->Errors[j] * Weights[i][j];
????????????}
????????????Errors[i] = sum * NeuronValues[i] *(1.0f - NeuronValues[i]);
????????}
????}
}
如果該層沒有子層(僅當(dāng)該層是輸出層時(shí)才會(huì)發(fā)生),則使用輸出層誤差的公式。如果該層沒有父層(僅當(dāng)該層是輸入層時(shí)才會(huì)發(fā)生),則錯(cuò)誤設(shè)置為 0。如果該層同時(shí)具有父層和子層,則它是隱藏層,隱藏層的公式應(yīng)用錯(cuò)誤。
示例 14-8 中所示的 AdjustWeights 方法負(fù)責(zé)計(jì)算對(duì)每個(gè)連接權(quán)重進(jìn)行的調(diào)整。
Example 14-8. AdjustWeights method?
void NeuralNetworkLayer::AdjustWeights(void)
{
????int i, j;
????double dw;
????if(ChildLayer != NULL)
????{
????????for(i=0; i<NumberOfNodes; i++)
????????{
????????????for(j=0; j<NumberOfChildNodes; j++)
????????????{
????????????????dw = LearningRate * ChildLayer->Errors[j] *
????????????????????????????NeuronValues[i];
????????????????if(UseMomentum)
????????????????{
????????????????????Weights[i][j] += dw + MomentumFactor *
????????????????????????????????????????????????????????????WeightChanges[i][j];
????????????????????WeightChanges[i][j] = dw;
????????????????} else {
????????????????????????Weights[i][j] += dw;
????????????????}
????????????}
????????}
????????for(j=0; j<NumberOfChildNodes; j++)
????????{
????????????BiasWeights[j] += LearningRate *ChildLayer->Errors[j] *BiasValues[j];
????????}
????}
}
僅當(dāng)該層具有子層時(shí),即該層是輸入層或隱藏層時(shí),才會(huì)調(diào)整權(quán)重。輸出層沒有子層,因此沒有連接和相關(guān)權(quán)重需要調(diào)整。嵌套的for循環(huán)循環(huán)遍歷層中的節(jié)點(diǎn)和子層中的節(jié)點(diǎn)。請記住,層中的每個(gè)神經(jīng)元都連接到子層中的每個(gè)節(jié)點(diǎn)。在這些嵌套循環(huán)中,權(quán)重調(diào)整是使用前面顯示的公式計(jì)算的。如果要應(yīng)用動(dòng)量,則動(dòng)量因子乘以前一個(gè) epoch 的權(quán)重變化也會(huì)被添加到權(quán)重變化中。然后將這個(gè)時(shí)期的權(quán)重變化存儲(chǔ)在下一個(gè)時(shí)期的 WeightChanges 數(shù)組中。如果不使用動(dòng)量,則在沒有動(dòng)量的情況下應(yīng)用重量變化,并且不需要存儲(chǔ)重量變化。
最后,以類似于連接權(quán)重的方式調(diào)整偏置權(quán)重。對(duì)于連接到子節(jié)點(diǎn)的每個(gè)偏置,調(diào)整等于學(xué)習(xí)率乘以子神經(jīng)元誤差乘以偏置值。
14.2.13 神經(jīng)網(wǎng)絡(luò)類
NeuralNetwork類封裝了NeuralNetworkLayer類的三個(gè)實(shí)例,一個(gè)用于網(wǎng)絡(luò)中的每一層:輸入層、隱藏層和輸出層。示例 14-9 顯示了類頭。
Example 14-9. NeuralNetwork class
class NeuralNetwork
{
public:
????NeuralNetworkLayer InputLayer;
????NeuralNetworkLayer HiddenLayer;
????NeuralNetworkLayer OutputLayer;
????void Initialize(int nNodesInput, int nNodesHidden,
????int nNodesOutput);
????void CleanUp();
????void SetInput(int i, double value);
????double GetOutput(int i);
????void SetDesiredOutput(int i, double value);
????void FeedForward(void);
????void BackPropagate(void);
????int GetMaxOutputID(void);
????double CalculateError(void);
????void SetLearningRate(double rate);
????void SetLinearOutput(bool useLinear);
????void SetMomentum(bool useMomentum, double factor);
????void DumpData(char* filename);
};
該類中只有三個(gè)成員對(duì)應(yīng)于構(gòu)成該類的層。但是,這個(gè)類包含 13 個(gè)方法,我們將在接下來進(jìn)行介紹。
Example 14-10? 顯示初始化方法
?
Example 14-10. Initialize method 初始化方法
void NeuralNetwork::Initialize(int nNodesInput,
????????????????????????????????????????????????????????????????????int nNodesHidden,
????????????????????????????????????????????????????????????????????int nNodesOutput)
{
????InputLayer.NumberOfNodes = nNodesInput;
????InputLayer.NumberOfChildNodes = nNodesHidden;
????InputLayer.NumberOfParentNodes = 0;
????InputLayer.Initialize(nNodesInput, NULL, &HiddenLayer);
????InputLayer.RandomizeWeights();
????HiddenLayer.NumberOfNodes = nNodesHidden;
????HiddenLayer.NumberOfChildNodes = nNodesOutput;
????HiddenLayer.NumberOfParentNodes = nNodesInput;
????HiddenLayer.Initialize(nNodesHidden,&InputLayer,&OutputLayer);
????HiddenLayer.RandomizeWeights();
????OutputLayer.NumberOfNodes = nNodesOutput;
????OutputLayer.NumberOfChildNodes = 0;
????OutputLayer.NumberOfParentNodes = nNodesHidden;
????OutputLayer.Initialize(nNodesOutput, &HiddenLayer, NULL);
}
Initialize 采用三個(gè)參數(shù),對(duì)應(yīng)于構(gòu)成網(wǎng)絡(luò)的三層中每一層中包含的神經(jīng)元數(shù)量。這些參數(shù)初始化對(duì)應(yīng)于輸入層、隱藏層和輸出層的層類的實(shí)例。 Initialize 還處理在層之間建立正確的父子連接。此外,它繼續(xù)并隨機(jī)化連接權(quán)重。
示例 14-11 中所示的 CleanUp 方法只是為每個(gè)層實(shí)例調(diào)用 CleanUp 方法。
Example 14-11. CleanUp method 清理方法
void NeuralNetwork::CleanUp()
{
????InputLayer.CleanUp();
????HiddenLayer.CleanUp();
????OutputLayer.CleanUp();
}
SetInput 用于設(shè)置特定輸入神經(jīng)元的輸入值。示例 14-12 顯示了 SetInput 方法。
Example 14-12. SetInput method 設(shè)置輸入法
void NeuralNetwork::SetInput(int i, double value)
{
????if((i>=0) && (i<InputLayer.NumberOfNodes))
????{
????????InputLayer.NeuronValues[i] = value;
????}
}
SetInput 有兩個(gè)參數(shù),對(duì)應(yīng)于將為其設(shè)置輸入的神經(jīng)元的索引和輸入值本身。該信息然后用于設(shè)置特定輸入。您可以在訓(xùn)練期間使用此方法設(shè)置訓(xùn)練集輸入,并在網(wǎng)絡(luò)的現(xiàn)場使用期間設(shè)置將計(jì)算輸出的輸入數(shù)據(jù)。
Example 14-13. GetOutput method
double NeuralNetwork::GetOutput(int i)
{
????if((i>=0) && (i<OutputLayer.NumberOfNodes))
????{
????????return OutputLayer.NeuronValues[i];
????}
????return (double) INT_MAX; // to indicate an error
}
GetOutput 有一個(gè)參數(shù),即我們需要輸出值的輸出神經(jīng)元的索引。該方法返回指定輸出神經(jīng)元的值或激活。請注意,如果您指定的索引超出有效輸出神經(jīng)元的范圍,則將返回 INT_MAX 以指示錯(cuò)誤。
在訓(xùn)練期間,我們需要將計(jì)算的輸出與期望的輸出進(jìn)行比較。圖層類有助于計(jì)算以及存儲(chǔ)所需的輸出值。示例 14-14 中所示的 SetDesiredOutput 方法用于幫助將所需的輸出設(shè)置為對(duì)應(yīng)于一組給定輸入的值。
Example 14-14. SetDesiredOutput method SetDesiredOutput 方法
void NeuralNetwork::SetDesiredOutput(int i, double value)
{
????if((i>=0) && (i<OutputLayer.NumberOfNodes))
????{
????????OutputLayer.DesiredValues[i] = value;
????}
}
SetDesiredOutput 有兩個(gè)參數(shù),對(duì)應(yīng)于設(shè)置期望輸出的輸出神經(jīng)元的索引和期望輸出本身的值。
為了讓網(wǎng)絡(luò)在給定一組輸入的情況下實(shí)際生成輸出,我們需要調(diào)用示例 14-15 中所示的 FeedForward 方法
Example 14-15. FeedForward method 前饋方法
void NeuralNetwork::FeedForward(void)
{
????InputLayer.CalculateNeuronValues();
????HiddenLayer.CalculateNeuronValues();
????OutputLayer.CalculateNeuronValues();
}
此方法只是依次為輸入層、隱藏層和輸出層調(diào)用CalculateNeuronValues方法。這些調(diào)用完成后,輸出層將包含計(jì)算出的輸出,然后可以通過調(diào)用 GetOutput 方法對(duì)其進(jìn)行檢查。
在訓(xùn)練期間,一旦計(jì)算出輸出,我們就需要使用反向傳播技術(shù)調(diào)整連接權(quán)重。 BackPropagate 方法處理此任務(wù)。示例 14-16 顯示了 BackPropagate 方法。
14-16 顯示了 BackPropagate 方法
void NeuralNetwork::BackPropagate(void)
{
????OutputLayer.CalculateErrors();
????HiddenLayer.CalculateErrors();
????HiddenLayer.AdjustWeights();
????InputLayer.AdjustWeights();
}
BackPropagate 首先按順序?yàn)檩敵鰧雍碗[藏層調(diào)用CalculateErrors方法。然后它繼續(xù)按順序?yàn)殡[藏層和輸入層調(diào)用 AdjustWeights方法。順序在這里很重要,它必須是示例14-16中顯示的順序,即我們通過網(wǎng)絡(luò)向后工作而不是向前工作,如前饋情況.
當(dāng)使用具有多個(gè)輸出神經(jīng)元的網(wǎng)絡(luò)和贏者通吃的方法來確定哪個(gè)輸出被激活時(shí),您需要找出哪個(gè)輸出神經(jīng)元具有最高輸出值。例 14-17 中所示的 GetMaxOutputID 就是為此目的而提供的.
Example 14-17. GetMaxOutputID method 獲取最大輸出ID 方法
int NeuralNetwork::GetMaxOutputID(void)
{
????int i, id;
????double maxval;
????maxval = OutputLayer.NeuronValues[0];
????id = 0;
????for(i=1; i<OutputLayer.NumberOfNodes; i++)
????{
????????if(OutputLayer.NeuronValues[i] > maxval)
????????{
????????????maxval = OutputLayer.NeuronValues[i];
????????????id = i;
????????}
????}
return id;
}
GetMaxOutputID 簡單地遍歷所有輸出層神經(jīng)元以確定哪個(gè)具有最高輸出值。返回具有最高值的神經(jīng)元的索引。
之前我們討論了計(jì)算與給定輸出集相關(guān)的誤差的需要。出于培訓(xùn)目的,我們需要這樣做。CalculateError方法負(fù)責(zé)為我們計(jì)算誤差。示例 14-18 顯示了 CalculateError 方法。
Example 14-18. CalculateError method
double NeuralNetwork::CalculateError(void)
{
????int i;
????double error = 0;
????for(i=0; i<OutputLayer.NumberOfNodes; i++)
????{
????????error += pow(OutputLayer.NeuronValues[i] --
????????OutputLayer.DesiredValues[i], 2);
????}
????error = error / OutputLayer.NumberOfNodes;
????return error;
}
CalculateError 使用我們之前討論的均方誤差公式返回與計(jì)算輸出值和給定的一組期望輸出值相關(guān)聯(lián)的誤差值。
為方便起見,我們提供了 SetLearningRate方法,如示例14-19所示。您可以使用它來設(shè)置構(gòu)成網(wǎng)絡(luò)的每一層的學(xué)習(xí)率
Example 14-19. SetLearningRate method 設(shè)置學(xué)習(xí)率方法
void NeuralNetwork::SetLearningRate(double rate)
{
????InputLayer.LearningRate = rate;
????HiddenLayer.LearningRate = rate;
????OutputLayer.LearningRate = rate;
}
SetLinearOutput,如示例 14-20 所示,是另一種便捷方法。您可以使用它為網(wǎng)絡(luò)中的每一層設(shè)置 LinearOutput 標(biāo)志。但是請注意,在此實(shí)現(xiàn)中只有輸出層會(huì)使用線性激活。
Example 14-20. SetLinearOutput method 設(shè)置線性輸出方法
void NeuralNetwork::SetLinearOutput(bool useLinear)
{
????InputLayer.LinearOutput = useLinear;
????HiddenLayer.LinearOutput = useLinear;
????OutputLayer.LinearOutput = useLinear;
}
您使用 SetMomentum,如示例 14-21 所示,為網(wǎng)絡(luò)中的每一層設(shè)置 UseMomentum 標(biāo)志和動(dòng)量因子。
Example 14-21. SetMomentum method 設(shè)置動(dòng)量方法
void NeuralNetwork::SetMomentum(bool useMomentum, double factor)
{
????InputLayer.UseMomentum = useMomentum;
????HiddenLayer.UseMomentum = useMomentum;
????OutputLayer.UseMomentum = useMomentum;
????InputLayer.MomentumFactor = factor;
????HiddenLayer.MomentumFactor = factor;
????OutputLayer.MomentumFactor = factor;
}
DumpData 是一種方便的方法,它只是將網(wǎng)絡(luò)的一些重要數(shù)據(jù)流式傳輸?shù)捷敵鑫募?。示?14-22 顯示了 DumpData 方法。
Example 14-22 DumpData method 轉(zhuǎn)儲(chǔ)數(shù)據(jù)方法
void NeuralNetwork::DumpData(char* filename)
{
????FILE* f;
????int i, j;
????f = fopen(filename, "w");
????fprintf(f, "---------------------------------------------\n");
????fprintf(f, "Input Layer\n");
????fprintf(f, "---------------------------------------------\n");
????fprintf(f, "\n");
????fprintf(f, "Node Values:\n");
????fprintf(f, "\n");
????for(i=0; i<InputLayer.NumberOfNodes; i++)
????fprintf(f, "(%d) = %f\n", i, InputLayer.NeuronValues[i]);
????fprintf(f, "\n");
????fprintf(f, "Weights:\n");
????fprintf(f, "\n");
????for(i=0; i<InputLayer.NumberOfNodes; i++)
????for(j=0; j<InputLayer.NumberOfChildNodes; j++)
????fprintf(f, "(%d, %d) = %f\n", i, j,
????InputLayer.Weights[i][j]);
????fprintf(f, "\n");
????fprintf(f, "Bias Weights:\n");
????fprintf(f, "\n");
????for(j=0; j<InputLayer.NumberOfChildNodes; j++)
????fprintf(f, "(%d) = %f\n", j, InputLayer.BiasWeights[j]);
????fprintf(f, "\n");
????fprintf(f, "\n");
????fprintf(f, "---------------------------------------------\n");
????fprintf(f, "Hidden Layer\n");
????fprintf(f, "---------------------------------------------\n");
????fprintf(f, "\n");
????fprintf(f, "Weights:\n");
????fprintf(f, "\n");
????for(i=0; i<HiddenLayer.NumberOfNodes; i++)
????for(j=0; j<HiddenLayer.NumberOfChildNodes; j++)
????fprintf(f, "(%d, %d) = %f\n", i, j,
????HiddenLayer.Weights[i][j]);
????fprintf(f, "\n");
????fprintf(f, "Bias Weights:\n");
????fprintf(f, "\n");
????for(j=0; j<HiddenLayer.NumberOfChildNodes; j++)
????fprintf(f, "(%d) = %f\n", j, HiddenLayer.BiasWeights[j]);
????fprintf(f, "\n");
????fprintf(f, "\n");
????fprintf(f, "---------------------------------------------\n");
????fprintf(f, "Output Layer\n");
????fprintf(f, "---------------------------------------------\n");
????fprintf(f, "\n");
????fprintf(f, "Node Values:\n");
????fprintf(f, "\n");
????for(i=0; i<OutputLayer.NumberOfNodes; i++)
????fprintf(f, "(%d) = %f\n", i, OutputLayer.NeuronValues[i]);
????fprintf(f, "\n");
????fclose(f);
}
發(fā)送到給定輸出文件的數(shù)據(jù)包括構(gòu)成網(wǎng)絡(luò)的層的權(quán)重、值和偏置權(quán)重。當(dāng)您想要檢查給定網(wǎng)絡(luò)的內(nèi)部結(jié)構(gòu)時(shí),這很有用。這在調(diào)試時(shí)以及在您可能使用實(shí)用程序訓(xùn)練網(wǎng)絡(luò)并希望在實(shí)際游戲中硬編碼訓(xùn)練的權(quán)重而不是花時(shí)間在游戲中執(zhí)行初始訓(xùn)練的情況下很有用。對(duì)于后一個(gè)目的,您必須修改此處顯示的 NeuralNetwork 類,以便于從外部源加載權(quán)重。

14.4 Chasing and Evading with Brains 動(dòng)腦筋追逃
我們將在本節(jié)中討論的示例是我們在第 4 章中討論的集群和追逐示例的修改版。在那一章中,我們討論了一個(gè)示例,其中一群單位追逐玩家控制的單位。在這個(gè)修改過的例子中,計(jì)算機(jī)控制的單元將使用神經(jīng)網(wǎng)絡(luò)來決定是追逐玩家、躲避他,還是與其他計(jì)算機(jī)控制的單元一起蜂擁而至。此示例是游戲場景的理想化或近似,您在游戲中擁有可以讓玩家參與戰(zhàn)斗的生物或單位。您不想讓生物總是攻擊玩家,也不想使用有限狀態(tài)機(jī)“大腦”,而是希望使用神經(jīng)網(wǎng)絡(luò)不僅為生物做出決策,而且根據(jù)它們攻擊玩家的經(jīng)驗(yàn)來調(diào)整它們的行為。
下面是我們的簡單示例的工作方式。大約 20 個(gè)計(jì)算機(jī)控制單元將在屏幕上移動(dòng)。他們會(huì)攻擊玩家,從玩家身邊逃跑,或者與其他計(jì)算機(jī)控制的單位一起蜂擁而至。所有這些行為都將使用我們在前面章節(jié)中介紹的確定性算法來處理;然而,這里關(guān)于執(zhí)行什么行為的決定取決于神經(jīng)網(wǎng)絡(luò)。玩家可以隨心所欲地在屏幕上移動(dòng)。當(dāng)玩家和計(jì)算機(jī)控制的單位彼此進(jìn)入指定半徑范圍內(nèi)時(shí),我們將假設(shè)他們正在進(jìn)行戰(zhàn)斗。我們不會(huì)在這里實(shí)際模擬戰(zhàn)斗,而是使用一個(gè)基本系統(tǒng),計(jì)算機(jī)控制的單位在玩家的戰(zhàn)斗范圍內(nèi)時(shí),在游戲循環(huán)中每回合都會(huì)失去一定數(shù)量的生命值。玩家將失去一定數(shù)量的生命值,與戰(zhàn)斗范圍內(nèi)計(jì)算機(jī)控制的單位數(shù)量成正比。當(dāng)一個(gè)單位的生命值達(dá)到零時(shí),他會(huì)死亡并自動(dòng)重生。
所有計(jì)算機(jī)控制的單元共享同一個(gè)大腦——神經(jīng)網(wǎng)絡(luò)。隨著計(jì)算機(jī)控制的單元獲得玩家的經(jīng)驗(yàn),我們還將讓這個(gè)大腦進(jìn)化。我們將通過在游戲本身中實(shí)施反向傳播算法來實(shí)現(xiàn)這一點(diǎn),以便我們可以實(shí)時(shí)調(diào)整網(wǎng)絡(luò)的權(quán)重。我們假設(shè)計(jì)算機(jī)控制的單元共同進(jìn)化。
我們希望看到計(jì)算機(jī)控制的單位學(xué)會(huì)在玩家在戰(zhàn)斗中壓倒他們時(shí)避開玩家。相反,我們希望看到計(jì)算機(jī)控制的單位在知道他們手上有弱者時(shí)變得更具侵略性。另一種可能性是計(jì)算機(jī)控制的單元將學(xué)會(huì)留在群體或群體,他們更有可能擊敗玩家。
14.2.14 Initialization and Training 初始化和訓(xùn)練
以第 4 章的植絨示例為起點(diǎn),我們要做的第一件事是添加一個(gè)新的全局變量,稱為 TheBrain,以表示神經(jīng)網(wǎng)絡(luò),如示例 14-23 所示。
Example 14-23. New global variable 新的全局變量
NeuralNetwork TheBrain;
我們必須在程序開始時(shí)初始化神經(jīng)網(wǎng)絡(luò)。這里,初始化包括配置和訓(xùn)練神經(jīng)網(wǎng)絡(luò)。前面示例中的 Initialize 函數(shù)顯然是處理神經(jīng)網(wǎng)絡(luò)初始化的地方,如示例 14-24 所示
Example 14-24. Initialization
void Initialize(void)
{
????int i;
????.
????.
????.
????for(i=0; i<_MAX_NUM_UNITS; i++)
????{
????????.
????????.
????????.
????????Units[i].HitPoints = _MAXHITPOINTS;
????????Units[i].Chase = false;
????????Units[i].Flock = false;
????????Units[i].Evade = false;
????}
????.
????.
????.
????Units[0].HitPoints = _MAXHITPOINTS;
????TheBrain.Initialize(4, 3, 3);
????TheBrain.SetLearningRate(0.2);
????TheBrain.SetMomentum(true, 0.9);
????TrainTheBrain();
}
此版本 Initialize 中的大部分代碼與前面的示例相同,因此我們在示例14-24的代碼清單中省略了它。剩下的代碼是我們添加的用于處理將神經(jīng)網(wǎng)絡(luò)合并到示例中的代碼。
請注意,我們必須向剛體結(jié)構(gòu)添加一些新成員,如示例14-25所示。這些新成員包括生命值的數(shù)量,以及指示該單位是在追逐、躲避還是蜂擁而至的標(biāo)志。
Example 14-25. RigidBody2D class?
class RigidBody2D {
public:
.
.
.
????double HitPoints;
????int NumFriends;
????int Command;
????bool Chase;
????bool Flock;
????bool Evade;
????double Inputs[4];
};
另請注意,我們添加了一個(gè)輸入向量。這用于在用于確定單元應(yīng)該采取什么動(dòng)作時(shí)將輸入值存儲(chǔ)到神經(jīng)網(wǎng)絡(luò);
回到示例 14-24 中的 Initialize方法,在單元初始化之后是處理TheBrain的時(shí)候了。我們做的第一件事是調(diào)用神經(jīng)網(wǎng)絡(luò)的 Initialize 方法,將代表每一層神經(jīng)元數(shù)量的值傳遞給它。在這種情況下,我們有四個(gè)輸入神經(jīng)元、三個(gè)隱藏神經(jīng)元和三個(gè)輸出神經(jīng)元。該網(wǎng)絡(luò)類似于圖 14-4 中所示的網(wǎng)絡(luò)。
接下來我們要做的是將學(xué)習(xí)率設(shè)置為0.2。我們通過反復(fù)試驗(yàn)調(diào)整了這個(gè)值,目的是在保持準(zhǔn)確性的同時(shí)減少訓(xùn)練時(shí)間。接下來我們調(diào)用SetMomentum方法表示我們要在訓(xùn)練時(shí)使用動(dòng)量,我們將動(dòng)量因子設(shè)置為 0.9
現(xiàn)在網(wǎng)絡(luò)已經(jīng)初始化,我們可以通過調(diào)用函數(shù) TrainTheBrain 來訓(xùn)練它。示例 14-26 顯示了 TrainTheBrain 函數(shù)。
Example 14-26. TrainTheBrain function 訓(xùn)練大腦功能
void TrainTheBrain(void)
{
????int i;
????double error = 1;
????int c = 0;
????TheBrain.DumpData("PreTraining.txt");
????while((error > 0.05) && (c<50000))
????{
????????error = 0;
????????c++;
????????for(i=0; i<14; i++)
????????{
????????????TheBrain.SetInput(0, TrainingSet[i][0]);
????????????TheBrain.SetInput(1, TrainingSet[i][1]);
????????????TheBrain.SetInput(2, TrainingSet[i][2]);
????????????TheBrain.SetInput(3, TrainingSet[i][3]);
????????????TheBrain.SetDesiredOutput(0, TrainingSet[i][4]);
????????????TheBrain.SetDesiredOutput(1, TrainingSet[i][5]);
????????????TheBrain.SetDesiredOutput(2, TrainingSet[i][6]);
????????????TheBrain.FeedForward();
????????????error += TheBrain.CalculateError();
????????????TheBrain.BackPropagate();
????????}
????????error = error / 14.0f;
????}
????TheBrain.DumpData("PostTraining.txt");
}
在我們開始訓(xùn)練網(wǎng)絡(luò)之前,我們將其數(shù)據(jù)轉(zhuǎn)儲(chǔ)到一個(gè)文本文件中,以便我們在調(diào)試期間可以參考它。接下來,我們進(jìn)入一個(gè)使用反向傳播算法訓(xùn)練網(wǎng)絡(luò)的 while 循環(huán)。 while 循環(huán)循環(huán)直到計(jì)算出的誤差小于某個(gè)指定值,或者直到迭代次數(shù)達(dá)到指定的最大閾值。后一種情況是為了防止 while 循環(huán)在永遠(yuǎn)不會(huì)達(dá)到錯(cuò)誤閾值的情況下永遠(yuǎn)循環(huán)。
在仔細(xì)研究 while 循環(huán)中發(fā)生的事情之前,讓我們先看一下用于訓(xùn)練該網(wǎng)絡(luò)的訓(xùn)練數(shù)據(jù)。稱為 TrainingSet 的全局?jǐn)?shù)組用于存儲(chǔ)訓(xùn)練數(shù)據(jù)。示例 14-27 顯示了訓(xùn)練數(shù)據(jù)。
Example 14-27. Training data 訓(xùn)練數(shù)據(jù)
double TrainingSet[14][7] = {
//#Friends, Hit points, Enemy Engaged, Range, Chase, Flock, Evade
0, 1, 0, 0.2, 0.9, 0.1, 0.1,
0, 1, 1, 0.2, 0.9, 0.1, 0.1,
0, 1, 0, 0.8, 0.1, 0.1, 0.1,
0.1, 0.5, 0, 0.2, 0.9, 0.1, 0.1,
0, 0.25, 1, 0.5, 0.1, 0.9, 0.1,
0, 0.2, 1, 0.2, 0.1, 0.1, 0.9,
0.3, 0.2, 0, 0.2, 0.9, 0.1, 0.1,
0, 0.2, 0, 0.3, 0.1, 0.9, 0.1,
0, 1, 0, 0.2, 0.1, 0.9, 0.1,
0, 1, 1, 0.6, 0.1, 0.1, 0.1,
0, 1, 0, 0.8, 0.1, 0.9, 0.1,
0.1, 0.2, 0, 0.2, 0.1, 0.1, 0.9,
0, 0.25, 1, 0.5, 0.1, 0.1, 0.9,
0, 0.6, 0, 0.2, 0.1, 0.1, 0.9
};
訓(xùn)練數(shù)據(jù)由 14 組輸入和輸出值組成。每組包含四個(gè)輸入節(jié)點(diǎn)的值,代表一個(gè)單位的朋友數(shù)量、它的生命值、敵人是否已經(jīng)交戰(zhàn)以及到敵人的距離。每個(gè)集合還包含三個(gè)輸出節(jié)點(diǎn)的數(shù)據(jù),對(duì)應(yīng)于追逐、聚集和逃避行為。
請注意,所有數(shù)據(jù)值都在 0.0 到 1.0 的范圍內(nèi)。如前所述,所有輸入數(shù)據(jù)都按比例縮放到 0.0 到 1.0 的范圍內(nèi),并且由于使用了邏輯輸出函數(shù),每個(gè)輸出值的范圍都在0.0到1.0之間。我們稍后會(huì)看到輸入數(shù)據(jù)是如何縮放的。至于輸出,輸出達(dá)到 0.0 或 1.0 是不切實(shí)際的,因此我們使用0.1表示非活動(dòng)輸出,0.9表示活動(dòng)輸出。另請注意,這些輸出值表示相應(yīng)輸入數(shù)據(jù)集的所需輸出。
我們根據(jù)經(jīng)驗(yàn)選擇訓(xùn)練數(shù)據(jù)?;旧希覀兗僭O(shè)了一些任意輸入條件,然后指定對(duì)該輸入的合理響應(yīng)是什么,并相應(yīng)地設(shè)置輸出值。在實(shí)踐中,您可能會(huì)對(duì)此進(jìn)行更多思考,并且可能會(huì)使用比我們在此處為這個(gè)簡單示例所做的更多的訓(xùn)練集。
現(xiàn)在,讓我們回到例 14-26中處理反向傳播訓(xùn)練的while循環(huán)。進(jìn)入while循環(huán)后,錯(cuò)誤被初始化為 0。我們將計(jì)算每個(gè)時(shí)期的錯(cuò)誤,它由所有 14 組輸入和輸出值組成。對(duì)于每組數(shù)據(jù),我們設(shè)置輸入神經(jīng)元值和期望的輸出神經(jīng)元值,然后調(diào)用網(wǎng)絡(luò)的FeedForward方法。之后,我們就可以計(jì)算誤差了。為此,我們調(diào)用網(wǎng)絡(luò)的 CalculateError 方法并將結(jié)果累積到錯(cuò)誤變量中。然后我們通過調(diào)用 BackPropagate方法繼續(xù)調(diào)整連接權(quán)重。在一個(gè)時(shí)期完成這些步驟后,我們通過將誤差除以該時(shí)期中數(shù)據(jù)集的數(shù)量14來計(jì)算該時(shí)期的平均誤差。在訓(xùn)練結(jié)束時(shí),網(wǎng)絡(luò)的數(shù)據(jù)被轉(zhuǎn)儲(chǔ)到一個(gè)文本文件中供以后檢查。
此時(shí),神經(jīng)網(wǎng)絡(luò)已準(zhǔn)備就緒。您可以按原樣將其與經(jīng)過訓(xùn)練的連接權(quán)重一起使用。這將為您省去編寫有限狀態(tài)機(jī)等代碼以處理所有可能的輸入條件的麻煩。網(wǎng)絡(luò)的一個(gè)更有吸引力的應(yīng)用是允許它在運(yùn)行中學(xué)習(xí)。如果根據(jù)網(wǎng)絡(luò)做出的決定,這些單元表現(xiàn)良好,我們可以強(qiáng)化這種行為。另一方面,如果單元表現(xiàn)不佳,我們可以重新訓(xùn)練網(wǎng)絡(luò)以抑制不良決策。
14.2.0 Learning 學(xué)習(xí)
在本節(jié)中,我們將繼續(xù)查看實(shí)現(xiàn)神經(jīng)網(wǎng)絡(luò)的代碼,包括使用反向傳播算法在游戲中學(xué)習(xí)的能力。查看示例 14-28 中所示的 UpdateSimulation 函數(shù)。這是我們在第4章中討論的UpdateSimulation 函數(shù)的修改版本。為清楚起見,示例 14-28 僅顯示了對(duì)該函數(shù)的修改。
Example 14-28. Modified UpdateSimulation function 修改更新模擬功能
“此處有碼”
我們在修改后的 UpdateSimulation 函數(shù)中做的第一件新事情是計(jì)算當(dāng)前與目標(biāo)交戰(zhàn)的計(jì)算機(jī)控制單元的數(shù)量。在我們的簡單示例中,如果一個(gè)單位與目標(biāo)的距離在指定距離內(nèi),則該單位被認(rèn)為正在與目標(biāo)交戰(zhàn)。
一旦我們確定了參與單位的數(shù)量,我們就會(huì)從目標(biāo)中扣除與參與單位數(shù)量成比例的一定數(shù)量的生命值。如果目標(biāo)生命值達(dá)到零,則認(rèn)為目標(biāo)已被殺死并在屏幕中間重生。此外,kill 標(biāo)志設(shè)置為 true。
下一步是處理計(jì)算機(jī)控制的單元。對(duì)于這個(gè)任務(wù),我們輸入一個(gè) for 循環(huán)來循環(huán)遍歷所有計(jì)算機(jī)控制的單元。進(jìn)入循環(huán)后,我們計(jì)算當(dāng)前單元到目標(biāo)的距離。接下來我們檢查目標(biāo)是否被殺死。如果是,我們會(huì)檢查當(dāng)前單位相對(duì)于目標(biāo)的位置,即它是否在交戰(zhàn)范圍內(nèi)。如果是,我們將重新訓(xùn)練神經(jīng)網(wǎng)絡(luò)以加強(qiáng)追逐行為。從本質(zhì)上講,如果該單位正在與目標(biāo)交戰(zhàn)并且目標(biāo)死亡,我們會(huì)假設(shè)該單位正在做正確的事情并且我們會(huì)加強(qiáng)追逐行為以使其更具侵略性。
Example 14-29 顯示處理網(wǎng)絡(luò)再訓(xùn)練的函數(shù).
Example 14-29. ReTrainTheBrain function 重新訓(xùn)練大腦功能
{
? ? ?int? ? ? ? ? i;
? ? ?Vector? ? ? u;
? ? ?bool? ? ? ? ?kill = false;
? ? ?// calc number of enemy units currently engaging the target
? ? ?Vector d;
? ? ?Units[0].NumFriends = 0;
? ? ?for(i=1; i<_MAX_NUM_UNITS; i++)
? ? ?{
? ? ? ? ? d = Units[i].vPosition -- Units[0].vPosition;
? ? ? ? ? if(d.Magnitude() <= (Units[0].fLength *
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?_CRITICAL_RADIUS_FACTOR))
? ? ? ? ? ? ? ? ?Units[0].NumFriends++;
? ? ?}
? ? ?// deduct hit points from target
? ? ?if(Units[0].NumFriends > 0)
? ? ?{
? ? ? ? ? Units[0].HitPoints --= 0.2 * Units[0].NumFriends;
? ? ? ? ? if(Units[0].HitPoints < 0)
? ? ? ? ? {
? ? ? ? ? ? ? ?Units[0].vPosition.x = _WINWIDTH/2;
? ? ? ? ? ? ? ?Units[0].vPosition.y = _WINHEIGHT/2;
? ? ? ? ? ? ? ?Units[0].HitPoints = _MAXHITPOINTS;
? ? ? ? ? ? ? ?kill = true;
? ? ? ? ? }
? ? ?}
? ? ?// update computer-controlled units:
? ? ?for(i=1; i<_MAX_NUM_UNITS; i++)
? ? ?{
? ? ? ? ? u = Units[0].vPosition -- Units[i].vPosition;
? ? ? ? ? if(kill)
? ? ? ? ? {
? ? ? ? ? ? ? ?if((u.Magnitude() <= (Units[0].fLength *
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?_CRITICAL_RADIUS_FACTOR)))
? ? ? ? ? ? ? ?{
? ? ? ? ? ? ? ? ? ? ReTrainTheBrain(i, 0.9, 0.1, 0.1);
? ? ? ? ? ? ? ?}
? ? ? ? ? }
? ? ? ? ? // handle enemy hit points, and learning if required
? ? ? ? ? if(u.Magnitude() <= (Units[0].fLength *
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?_CRITICAL_RADIUS_FACTOR))
? ? ? ? ? {
? ? ? ? ? ? ? ?Units[i].HitPoints --= DamageRate;
? ? ? ? ? ? ? ?if((Units[i].HitPoints < 0))
? ? ? ? ? ? ? ?{
? ? ? ? ? ? ? ? ?Units[i].vPosition.x=GetRandomNumber(_WINWIDTH/2
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? --_SPAWN_AREA_R,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? _WINWIDTH/2+_SPAWN_AREA_R,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? false);
? ? ? ? ? ? ? ? ? Units[i].vPosition.y=GetRandomNumber(_WINHEIGHT/2
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? --_SPAWN_AREA_R,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? _WINHEIGHT/2+_SPAWN_AREA_R,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? false);
? ? ? ? ? ? ? ? ? Units[i].HitPoints = _MAXHITPOINTS/2.0;
? ? ? ? ? ? ? ? ? ReTrainTheBrain(i, 0.1, 0.1, 0.9);
? ? ? ? ? ? ? ?}
? ? ? ? ? } else {
? ? ? ? ? ? ? ?Units[i].HitPoints+=0.01;
? ? ? ? ? ? ? ?if(Units[i].HitPoints > _MAXHITPOINTS)
? ? ? ? ? ? ? ? ? ?Units[i].HitPoints = _MAXHITPOINTS;
? ? ? ? ? }
? ? ? ? ? // get a new command
? ? ? ? ? Units[i].Inputs[0] = Units[i].NumFriends/_MAX_NUM_UNITS;
? ? ? ? ? Units[i].Inputs[1] = (double) (Units[i].HitPoints/
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?_MAXHITPOINTS);
? ? ? ? ? Units[i].Inputs[2] = (Units[0].NumFriends>0 ? 1:0);
? ? ? ? ? Units[i].Inputs[3] = (u.Magnitude()/800.0f);
? ? ? ? ? TheBrain.SetInput(0, Units[i].Inputs[0]);
? ? ? ? ? TheBrain.SetInput(1, Units[i].Inputs[1]);
? ? ? ? ? TheBrain.SetInput(2, Units[i].Inputs[2]);
? ? ? ? ? TheBrain.SetInput(3, Units[i].Inputs[3]);
? ? ? ? ? TheBrain.FeedForward();
? ? ? ? ? Units[i].Command = TheBrain.GetMaxOutputID();
? ? ? ? ? switch(Units[i].Command)
? ? ? ? ? {
? ? ? ? ? ? ? ?case 0:
? ? ? ? ? ? ? ? ? ? Units[i].Chase = true;
? ? ? ? ? ? ? ? ? ? Units[i].Flock = false;
? ? ? ? ? ? ? ? ? ? Units[i].Evade = false;
? ? ? ? ? ? ? ? ? ? Units[i].Wander = false;
? ? ? ? ? ? ? ? ? ? break;
? ? ? ? ? ? ? ?case 1:
? ? ? ? ? ? ? ? ? ? Units[i].Chase = false;
? ? ? ? ? ? ? ? ? ? Units[i].Flock = true;
? ? ? ? ? ? ? ? ? ? Units[i].Evade = false;
? ? ? ? ? ? ? ? ? ? Units[i].Wander = false;
? ? ? ? ? ? ? ? ? ? break;
? ? ? ? ? ? ? ?case 2:
? ? ? ? ? ? ? ? ? ? Units[i].Chase = false;
? ? ? ? ? ? ? ? ? ? Units[i].Flock = false;
? ? ? ? ? ? ? ? ? ? Units[i].Evade = true;
? ? ? ? ? ? ? ? ? ? Units[i].Wander = false;
? ? ? ? ? ? ? ? ? ? break;
? ? ? ? ? }
? ? ? ? ? DoUnitAI(i);
? ? ?} // end i-loop
? ? ?kill = false;
}
我們在修改后的 UpdateSimulation 函數(shù)中做的第一件新事情是計(jì)算當(dāng)前與目標(biāo)交戰(zhàn)的計(jì)算機(jī)控制單元的數(shù)量。 在我們的簡單示例中,如果一個(gè)單位與目標(biāo)的距離在指定距離內(nèi),則該單位被認(rèn)為正在與目標(biāo)交戰(zhàn)。
一旦我們確定了參與單位的數(shù)量,我們就會(huì)從目標(biāo)中扣除與參與單位數(shù)量成比例的一定數(shù)量的生命值。 如果目標(biāo)生命值達(dá)到零,則認(rèn)為目標(biāo)已被殺死并在屏幕中間重生。 此外,kill 標(biāo)志設(shè)置為 true。
下一步是處理計(jì)算機(jī)控制的單元。 對(duì)于這個(gè)任務(wù),我們輸入一個(gè) for 循環(huán)來循環(huán)遍歷所有計(jì)算機(jī)控制的單元。 進(jìn)入循環(huán)后,我們計(jì)算當(dāng)前單元到目標(biāo)的距離。 接下來我們檢查目標(biāo)是否被殺死。 如果是,我們會(huì)檢查當(dāng)前單位相對(duì)于目標(biāo)的位置——也就是說,它是否在交戰(zhàn)范圍內(nèi)。 如果是,我們將重新訓(xùn)練神經(jīng)網(wǎng)絡(luò)以加強(qiáng)追逐行為。 從本質(zhì)上講,如果該單位正在與目標(biāo)交戰(zhàn)并且目標(biāo)死亡,我們會(huì)假設(shè)該單位正在做正確的事情并且我們會(huì)加強(qiáng)追逐行為以使其更具攻擊性。
示例 14-29 顯示了處理網(wǎng)絡(luò)再訓(xùn)練的函數(shù)。
示例 14-29。 ReTrainTheBrain 函數(shù)
ReTrainTheBrain簡單地再次實(shí)現(xiàn)反向傳播訓(xùn)練算法,但這次將給定單元的存儲(chǔ)輸入和指定的目標(biāo)輸出用作訓(xùn)練數(shù)據(jù)。此處請注意,您不想將 while 循環(huán)的最大迭代閾值設(shè)置得太高。如果這樣做,在進(jìn)行再訓(xùn)練過程時(shí),動(dòng)作中可能會(huì)出現(xiàn)明顯的停頓。此外,如果您嘗試重新訓(xùn)練網(wǎng)絡(luò)以實(shí)現(xiàn)非常小的錯(cuò)誤,它會(huì)適應(yīng)得太快。您可以通過改變錯(cuò)誤和最大迭代閾值來控制網(wǎng)絡(luò)適應(yīng)的速率。
UpdateSimulation 函數(shù)的下一步是處理當(dāng)前單位的生命值。如果當(dāng)前單位在目標(biāo)的交戰(zhàn)范圍內(nèi),我們會(huì)從該單位中扣除規(guī)定數(shù)量的生命值。如果該單位的生命值達(dá)到零,我們假設(shè)它在戰(zhàn)斗中死亡,在這種情況下我們會(huì)在某個(gè)隨機(jī)位置重生它。我們還假設(shè)這個(gè)單位做錯(cuò)了什么,所以我們重新訓(xùn)練這個(gè)單位逃避而不是追逐。
現(xiàn)在我們繼續(xù)使用神經(jīng)網(wǎng)絡(luò)為單位做出決定——也就是說,在當(dāng)前的一組條件下,單位應(yīng)該追逐、聚集還是躲避。 為此,我們首先設(shè)置神經(jīng)網(wǎng)絡(luò)的輸入。 第一個(gè)輸入值是當(dāng)前單位的好友數(shù)。 我們通過將單位常量的最大數(shù)量除以該單位的朋友數(shù)量來縮放朋友數(shù)量。 第二個(gè)輸入是單位的生命值數(shù),它是通過將最大生命值數(shù)除以單位的生命值數(shù)來換算的。 第三個(gè)輸入是關(guān)于目標(biāo)是否參與的指示。 如果目標(biāo)已參與,則此值設(shè)置為 1.0,如果未參與,則設(shè)置為 0.0。 最后,第四個(gè)輸入是到目標(biāo)的距離。 在這種情況下,通過將屏幕寬度(假設(shè)為 800 像素)劃分為范圍來縮放當(dāng)前單元到目標(biāo)的距離。
設(shè)置所有輸入后,將通過調(diào)用FeedForward方法傳播網(wǎng)絡(luò)。調(diào)用之后,可以檢查網(wǎng)絡(luò)的輸出值以得出正確的行為。為此,我們選擇具有最高激活的輸出,這是通過調(diào)用 GetmaxOutputID 方法確定的。然后在 switch 語句中使用此 ID 為該單元設(shè)置適當(dāng)?shù)男袨闃?biāo)志。如果 ID 為 0,則該單位應(yīng)追逐。如果 ID 為 1,則該單位應(yīng)聚集。如果 ID 為 2,則該單元應(yīng)回避。
這會(huì)處理修改后的 UpdateSimulation 函數(shù)。如果你運(yùn)行這個(gè)示例程序,它可以從本書的網(wǎng)站 (http://www.oreilly.com/catalog/ai) 獲得,你會(huì)看到計(jì)算機(jī)控制單元的行為確實(shí)隨著模擬的運(yùn)行而調(diào)整。您可以使用數(shù)字鍵來控制目標(biāo)對(duì)單位造成的傷害程度。 1 鍵對(duì)應(yīng)很少或沒有損壞,而 8 鍵對(duì)應(yīng)大量損壞。如果你讓目標(biāo)在沒有對(duì)單位造成傷害的情況下死亡,你會(huì)發(fā)現(xiàn)他們很快就會(huì)適應(yīng)更頻繁的攻擊。如果你設(shè)置目標(biāo)使其造成巨大傷害,你會(huì)看到單位開始適應(yīng)以更多地避開目標(biāo)。他們也開始更頻繁地參與團(tuán)體活動(dòng),而不是作為個(gè)人參與。最終,他們適應(yīng)不惜一切代價(jià)避開目標(biāo)。從這個(gè)例子中產(chǎn)生的一個(gè)有趣的涌現(xiàn)行為是,單位傾向于形成羊群,而領(lǐng)導(dǎo)者傾向于出現(xiàn)。通常會(huì)形成一個(gè)羊群,領(lǐng)先的單位可能會(huì)追逐或躲避,而中間和尾隨的單位會(huì)跟在他們的前面。

14.5 Further Information 更多信息
正如我們在本章開頭所說的那樣,神經(jīng)網(wǎng)絡(luò)的主題過于龐大,無法用一章來解決。因此,我們編制了一份簡短的參考資料清單,如果您決定進(jìn)一步研究這個(gè)主題,您可能會(huì)發(fā)現(xiàn)它們很有用。名單如下:
Practical Neural Network Recipes in C++ (Academic Press)
Neural Networks for Pattern Recognition (Oxford University Press)
AI Application Programming (Charles River Media)
還有許多其他關(guān)于神經(jīng)網(wǎng)絡(luò)的書籍;然而,我們上面列出的那些對(duì)我們非常有用,尤其是第一個(gè),C++中的實(shí)用神經(jīng)網(wǎng)絡(luò)食譜。本書針對(duì)各種應(yīng)用程序的神經(jīng)網(wǎng)絡(luò)編程和使用提供了大量實(shí)用技巧和建議