強化學習之蒙特卡洛學習,時序差分學習理論與實戰

ai52learn發表於2020-12-10

目錄

  • 簡介

  • 蒙特卡洛強化學習

  • 時序差分強化學習

  • MC學習和TD學習的區別

  • n步時序差分學習

  • 程式設計實踐

  • 參考

蒙特卡洛強化學習

  • 蒙特卡洛強化學習(Monte-Carlo Reinforcement Learning,MC學習):指在不清楚MDP狀態轉移概率的情況下,直接從經歷完整的狀態序列(episode)來估計狀態的真實價值,並認為某狀態的價值等於在多個狀態序列中以該狀態算得到的所有收穫的平均值.

  • 完整的狀態序列(complete episode):指從某一個狀態開始,個體與環境互動直到終止狀態的獎勵為止.完整的狀態序列不要求起始狀態一定是某一個特定的狀態,但是要求個體最終進入環境認可的某一個終止狀態.

  • 蒙特卡洛強化學習有如下的特點:不依賴狀態轉移概率(即不依賴模型),直接從經歷過的完整的狀態序列中學習,使用的思想就是用平均收穫值代替價值.理論上完整的狀態序列越多,結果越準確.

  • 我們可以使用蒙特卡洛強化學習來評估一個給定的策略.基於特定策略的一個狀態序列資訊可以表示為如下的一個序列(即在初始狀態執行某動作,獲得離開該狀態的即時獎勵,到達下一個狀態):圖片

  • 其中時刻狀態的收穫可以表述為:

  • 圖片

  • 其中時刻為終止時刻.該策略下某一狀態的價值:

  • 圖片

  • 在蒙特卡洛演算法評估策略的時候要針對多個包含同一狀態的完整狀態序列求收穫繼而求收穫的平均值.在狀態轉移過程中,某一需要計算的狀態可能出現在序列的多個位置,也就是說個體在與環境互動的過程中從某狀態出發後又一次或多次返回該狀態.根據收穫的定義,不同時刻的同一狀態其計算得到的收穫值是不一樣的.

  • 我們有兩種方法可以選擇,一是僅把狀態序列中第一次出現該狀態時收穫值納入到收穫平均值的計算中;另一種是針對一個狀態序列中每次出現的該狀態,都計算對應的收穫值並納入收穫平均值的計算當中.兩種方法對應的蒙特卡洛評估分別稱為首次訪問(first visit)和每次訪問(every visit)蒙特卡洛評估.

  • 在求解狀態收穫的平均值的過程中,我們介紹一種非常實用的,不需要儲存所有歷史收穫的計算方法:累進更新平均值(incremental mean).而且這種計算平均值的思想也是強化學習的一個核心思想之一.具體公式如下:

  • 圖片

  • 累進更新平均值利用前一次的平均值和當前資料以及資料總個數來計算新的平均值:當每產生一個需要平均的新資料時,先計算與先前平均值的差,再將這個差值乘以一定的係數後作為誤差對舊平均值進行修正.如果該式中平均值和新資料分別看成是狀態的價值和該狀態的收穫,那麼該公式就變成了遞增式蒙特卡洛法更新狀態價值.公式如下:

  • 圖片

  • 在一些實時或者無法統計準確狀態被訪問次數時,可以用一個係數來代替狀態計數的倒數,公式變成:

  • 圖片

  • 以上就是蒙特卡洛學習方法的思想和描述,下文將介紹另一種強化學習方法:時序差分學習法

時序差分強化學習

  • 時序差分強化學習(Temporal-Difference reinforcement Learning,TD學習):指從取樣得到的不完整的狀態序列學習,該方法通過引導(bootstrapping),先估計某狀態在該狀態序列完整後可能獲得的收穫,並在此基礎上利用前文所屬的累進更新平均值的方法得到該狀態的價值,再通過不斷的取樣來持續更新這個價值.

  • 具體地說,在TD學習中,演算法在估計某一個狀態的收穫時,用的是離開該狀態的即刻獎勵與下一時刻狀態的預估狀態價值乘以衰減係陣列成,這符合貝爾曼方程的描述:

  • 圖片

  • 其中稱為TD目標值.稱為TD誤差.

  • 引導(bootstrapping):指的是用TD目標值代替收穫的過程

  • 可以看出,不管是MC學習還是TD學習,它們都不再需要清楚某一狀態的所有可能的後續狀態以及對應的狀態轉移概率,因此也不用像動態規劃演算法那樣進行全寬度的回溯來更新狀態的價值.這在解決大規模問題或者不清楚環境動力學特徵的問題時十分有效.不過MC學習和TD學習兩者也是有著很明顯的差別的.下文通過一個例子來詳細闡述這兩種學習方法的特點.

MC學習和TD學習的區別

  • 想象一下作為個體的你如何預測下班後開車回家這個行程所花費的時間.在回家的路上你會依次經過一段高速公路,普通公路和你家附近街區三段路程.由於你經常開車上下班,在下班的路上多次碰到過各種情形,比如取車的時候發現下雨,高速路況的好壞,普通公路是否堵車等等.在每一種狀態下時,你對還需要多久才能到家都有一個經驗性的估計.下表的既往經驗預計的仍需好時列給出了這個經驗估計,這個經驗估計基本反映了各個狀態對應的價值,通過你對下班回家總耗時的預估是30分鐘.圖片

  • 假設你現在又下班準備回家了,當花費了5分鐘從辦公室到車旁時,發現下雨了。此時根據既往經驗,估計還需要35分鐘才能到家,因此整個行程將耗費40分鐘。隨後你進入了高速公路,高速公路路況非常好,你一共僅用了20分鐘就離開了高速公路,通常根據經驗你只再需要15分鐘就能到家,加上已經過去的20分鐘,你將這次返家預計總耗時修正為35分鐘,比先前的估計少了5分鐘。但是當你進入普通公路時,發現交通流量較大,你不得不跟在一輛卡車後面龜速行駛,這個時候距離出發已經過去30 分鐘了,根據以往你路徑此段的經驗,你還需要10分鐘才能到家,那麼現在你對於回家總耗時的預估又回到了40分鐘。最後你在出發40分鐘後到達了家附近的街區,根據經驗,還需要3分鐘就能到家,此後沒有再出現新的情況,最終你在43分鐘的時候到達家中。經歷過這一次的下班回家,你對於處在途中各種狀態下返家的還需耗時(對應於各狀態的價值)有了新的估計,但分別使用MC演算法和TD演算法得到的對於各狀態返家還需耗時的更新結果和更新時機都是不一樣的。

  • 如果使用MC演算法,在整個駕車返家的過程中,你對於所處的每一個狀態,例如“取車時下雨”,“離開高速公路”,“被迫跟在卡車後”、“進入街區”等時,都不會立即更新這些狀態對應的返家還需耗時的估計,這些狀態的返家仍需耗時仍然分別是先前的35 分鐘、15分鐘、10分鐘和3分鐘。但是當你到家發現整個行程耗時43分鐘後,通過用實際總耗時減去到達某狀態的已耗時,你發現在本次返家過程中在實際到達上述各狀態時,仍需時間則分別變成了:38分鐘、23分鐘、13分鐘和3分鐘。如果選擇修正係數為1,那麼這些新的耗時將成為今後你在各狀態時的預估返家仍需耗時,相應的整個行程的預估耗時被更新為43分鐘

  • 如果使用TD演算法,則又是另外一回事,當取車發現下雨時,同樣根據經驗你會認為還需要35分鐘才能返家,此時,你將立刻更新對於返家總耗時的估計,為仍需的35分鐘加上你離開辦公室到取車現場花費的5分鐘,即40分鐘。同樣道理,當駛離高速公路,根據經驗,你對到家還需時間的預計為15分鐘,但由於之前你在高速上較為順利,節省了不少時間,在第20分鐘時已經駛離高速,實際從取車到駛離高速只花費了15分鐘,則此時你又立刻更新了從取車時下雨到到家所需的時間為30分鐘,而整個回家所需時間更新為35分鐘。當你在駛離高速在普通公路上又行駛了10 分鐘被堵,你預計還需10分鐘才能返家時,你對於剛才駛離高速公路返家還需耗時又做了更新,將不再是根據既往經驗預估的15 分鐘,而是現在的20分鐘,加上從出發到駛離高速已花費的20分鐘,整個行程耗時預估因此被更新為40分鐘。直到你花費了40分鐘只到達家附近的街區還預計有3分鐘才能到家時,你更新了在普通公路上對於返家還需耗時的預計為13分鐘。最終你按預計3 分鐘後進入家門,不再更新剩下的仍需耗時。

  • 通過比較可以看出,MC演算法只在整個行程結束後才更新各個狀態的仍需耗時,而TD演算法則每經過一個狀態就會根據在這個狀態與前一個狀態間實際所花時間來更新前一個狀態的仍需耗時.下圖用折線圖直觀的顯示了分別使用MC和TD演算法時預測的駕車回家總耗時的區別.需要注意的是,在這個例子中,與各狀態價值相對應的指標並不是圖中顯示的駕車返家總耗時,而是處於某個狀態時駕車返家的仍需耗時.圖片

  • TD學習能比MC學習更快速靈活的更新狀態的價值估計,這在一些情況下是非常重要的.回到駕車回家這個例子中來,我們給駕車回家制定一個新的目標,不再以耗時多少來評估狀態價值,而是要求安全平穩的返回家中.假如有一次你在駕車回家的路上突然碰到險情:對面開過來一輛車準備跟你相撞,不過由於雙方駕駛員都採取了緊急措施沒有讓險情實際發生,最後平安到家.如果用MC學習,路上發生的這一險情可能引發的極大負值獎勵將不會被考慮,你不會更新在碰到此類險情時狀態的價值;但在TD學習時,碰到這樣的險情過後,你會大幅調低這個狀態的價值,並在今後再次碰到類似情況時採取其他行為,例如降低速度等來讓自身處在一個價值較高的狀態中,儘可能避免發生意外事件的發生.

  • 通過這個例子,我們認識到:TD學習在知道結果之前就可以學習,也可以在沒有結果時學習,還可以在持續進行的環境中學習,而MC學習則要等到最後結果才能學習.

  • TD學習在更新狀態價值時使用的是TD目標值,即基於即時獎勵和下一狀態的預估價值來替代當前狀態在狀態序列結束時可能得到的收穫,它是當前狀態價值的有偏估計,而MC學習則使用實際的收穫來更新狀態價值,是基於某一策略下狀態價值的無偏估計.TD學習存在偏倚(bias)的原因是在於其更新價值時使用的也是後續狀態預估的價值,如果能使用後續狀態基於某策略的真實TD目標值(true TD target)來更新當前狀態價值的話,那麼此時的TD學習得到的價值也是實際價值的無偏估計.雖然絕大多數情況下TD學習得到的價值是有偏估計的,但是其方差(Variance)卻比MC學習得到的方法要低,且對初始值敏感,通常比MC學下更加高效,這也主要得益於TD學習價值更新靈活,對初始狀態價值的依賴較大.

  • 這裡的偏倚指的是距離期望的距離,預估的平均值與實際平均值的偏離程度;變異性指的是方差,評估單次取樣結果相對於與平均值變動的範圍大小。基本就是統計學上均值與方差的概念。

  • 假設在一個強化學習問題中有A,B兩個狀態,模型未知,不涉及策略和行為,只涉及狀態轉換和即時獎勵,衰減係數為1.現有如下表所示8個完整狀態序列的經歷,其中除了第1個狀態序列發生了狀態轉移外,其餘7個完整的狀態序列均只有一個狀態構成.現要求根據現有資訊計算狀態A,B的價值分別是多少?

  • 圖片

  • 我們考慮分別使用MC演算法和TD演算法來計算狀態A,B的價值.首先考慮MC演算法,在8個完整的狀態序列中,只有第一個序列中包含狀態A,因此A價值僅能通過第一個序列來計算,也就等同於計算該序列中狀態A的收穫:圖片

  • 狀態B的價值,則需要通過狀態B在8個序列中的收穫值來平均,其結果是6/8.因此在使用MC演算法時,狀態A,B的價值分別為6/8和0.

  • 再來考慮應用TD演算法,TD演算法在計算狀態序列中某狀態價值時是應用其後續狀態的預估價值來計算的,在8個狀態序列中,狀態B總是出現在終止狀態,因而直接使用終止狀態時獲得的獎勵來計算價值再針對狀態序列數做平均,這樣得到的狀態B的價值依然是6/8.狀態A由於只存在於第一個狀態序列中,因此直接使用包含狀態B的TD目標值來得到狀態A的價值,由於狀態A的即時獎勵為0,因而計算得到的狀態A的價值與B的價值相同,均為6/8.

  • TD演算法在計算狀態價值時利用了狀態序列中前後狀態之間的關係,由於已知資訊僅有8個完整序列,而且狀態A的後續狀態100%是狀態B,而狀態B始終作為終止狀態,有1/4獲得獎勵0,3/4獲得獎勵1.符合這樣的狀態轉移概率的MDP如下圖所示.圖片

  • 可以看出,TD演算法試圖構建一個MDP並使得這個MDP儘可能的符合已經產生的狀態序列,也就是說TD演算法將首先根據已有經驗估計狀態間的轉移概率:

  • 圖片

  • 同時估計一某一個狀態的即時獎勵:

  • 圖片

  • 最後計算MDP的狀態函式.

  • 而MC演算法則直接依靠完整狀態序列的獎勵得到的各狀態對應的收穫來計算狀態價值,因而這種演算法是以最小化收穫與狀態價值之間均方差為目標的:

  • 圖片

  • 通過上面的示例,我們能體會到TD演算法與MC演算法之間的另一個差別:TD演算法使用了MDP問題的馬爾科夫屬性,在具有馬爾科夫性的環境下更有效;但是MC演算法並不利用馬爾科夫屬性,適用範圍不限於具有馬爾科夫性的環境.

小結

  • 本章闡述的MC演算法,TD演算法和上一章講述的動態規劃演算法都可以用來計算狀態價值.他們的特點也是十分鮮明的,前兩種是在不依賴模型的情況下的常用方法,這其中又以MC學習需要完整的狀態序列來更新狀態價值,TD學習則不需要完整的狀態序列;DP演算法則是基於模型的計算狀態價值的方法,它通過計算一個狀態S所有可能的轉移狀態S'及其轉移概率以及對應的即時獎勵來計算這個狀態S的價值.

  • 在是否使用引導資料上,MC學習並不使用引導資料,它使用實際產生的獎勵值來計算狀態價值;TD和DP則都是用後續狀態的預估價值作為引導資料來計算當前狀態的價值.

  • 在是否取樣的問題上,MC和TD不依賴模型,使用的都是個體與環境實際互動產生的取樣狀態序列來計算狀態價值的,而DP則依賴狀態轉移概率矩陣和獎勵函式,全寬度計算狀態價值,沒有采樣之說.

  • MC演算法:深層取樣演算法,使用一次完整的狀態序列進行學習,使用實際收穫更新狀態預估價值,如下圖所示:

  • 圖片

  • TD演算法:淺層取樣演算法.經歷可以不完整,使用後續狀態的預估狀態價值預估收穫再更新當前狀態價值.如下圖所示:

  • 圖片

  • DP演算法:淺層全寬度學習.依據模型,全寬度地使用後續狀態預估價值來更新當前狀態價值.如下圖所示:

  • 圖片

  • 綜合以上,可以小結:當使用單個取樣,同時不經歷完整的狀態序列更新價值的演算法是TD學習;當使用單個取樣,但依賴完整狀態序列的演算法是MC演算法;當考慮全寬度取樣,但對每一個取樣經歷只考慮後續一個狀態時的演算法是DP學習;如果既考慮所有狀態轉移的可能性,同時又依賴完整狀態序列的,那麼這種演算法是窮舉法(exhausive search).需要說明的是:DP利用的是整個MDP問題的模型,也就是狀態轉移概率,雖然它並不實際利用取樣經歷,但它利用了整個模型的規律,因此也被認為是全寬度取樣的.圖片

n步時序差分學習

  • 第二節介紹的TD演算法實際上都是TD(0)演算法,括號內的數字0表示的是在當前狀態下往前多看1步,要是往前多看2步更新狀態價值會怎麼樣?這就引入了n-步預測的概念.

  • n-步預測指從狀態序列的當前狀態()開始往序列終止狀態方向觀察至狀態,使用這n個狀態產生的即時獎勵()以及狀態的預估價值來計算當前的狀態的價值

  • 圖片

  • TD是TD(0)的簡寫,是基於1-步預測的.根據n-步預測的定義,可以推出n=1,2和時對應的預測值如下表所示.從該表可以看出,MC學習是基於步預測的

  • 圖片

  • 定義n-步收穫為:

  • 圖片

  • 由此可以得到n-步TD學習對應的狀態價值函式的更新公式為:

  • 圖片

  • 我們可以知道,當n=1時等同於TD(0)學習,n取無窮大時等同於MC學習.由於TD學習和MC學習又各有優劣,那麼會不會存在一個n值使得預測能夠充分利用兩種學習的優點或者得到一個更好的預測效果呢?研究認為不同的問題其對應的比較高效的步數不是一成不變的.選擇多少步數作為一個較優的計算引數是需要嘗試的超引數調優問題.

  • 為了能在不增加計算複雜度的情況下綜合考慮所有步數的預測(即將不同n下的n步回報值做加權平均,構成一個有效的回報值),我們引入了一個新的引數,並定義收穫為:

  • 從n=1到的所有步收穫的權重之和.其中,任意一個n-步收穫的權重被設計為,如下圖所示.

  • 圖片

  • 通過這樣的權重設計,可以得到收穫的計算公式為:

  • 圖片

  • 對應的TD()被描述為:

  • 圖片

  • 下圖顯示了TD()對於n-收穫的權重分配,左側陰影部分是3-步收穫的權重值,隨著n的增大,其n-收穫的權重呈幾何級數的衰減.當在T時刻到達終止狀態時,未分配的權重(右側陰影部分)全部給予終止狀態的實際收穫值.如此設計可以使一個完整的狀態序列中所有n-步收穫的權重加起來為1,離當前狀態越遠的收穫其權重越小.

  • TD()的設計使得在一個episode中,後一個狀態的狀態價值與之前所有狀態的狀態價值有關,同時也可以說成是一個狀態價值參與決定了後續所有狀態的狀態價值。但是每個狀態的價值對於後續狀態價值的影響權重是不同的。

  • 前向認識TD:TD的設計使得在狀態序列中,一個狀態的價值由得到,而後者又間接由所有後續狀態價值計算得到,因此可以認為更新一個狀態的價值需要知道所有後續狀態的價值.也就是說,必須要經歷完整的狀態序列獲得包括終止狀態的每一個狀態的即時獎勵才能更新當前狀態的價值.這和MC演算法的要求一樣,因此TD演算法有著和MC演算法一樣的劣勢.取值區間為,當對應的就是MC演算法.這個給實際計算帶來了不便.這裡可以用一個例子方便大家理解:前向認識就假設一個人坐在狀態流上拿著望遠鏡看向前方,前方是那些將來的狀態。當估計當前狀態的值函式時,從TD(λ)的定義中可以看到,它需要用到將來時刻的值函式。

  • 圖片

  • 反向認識TD為TD演算法進行線上實時單步更新學習提供了理論依據.為了解釋這一點,需要引入效用跡這個概念.我們通過一個之前的例子來解釋這個問題,如下圖所示:圖片

  • 老鼠在依次連續接受了3次響鈴和1次亮燈訊號後遭到了電擊,那麼在分析遭電擊的原因時,到底是響鈴的因素較重要還是亮燈的因素更重要呢?

  • 如果把老鼠遭到電擊的原因認為是之前接受了較多次數的響鈴,則稱這種歸因為頻率啟發式(frequency heuristic);而把電擊歸因於最近少數幾次狀態的影響,則稱為就近啟發(recncy heuristic)式.如果給每一個狀態引入一個數值:效用(eligibility,E)來表示該狀態對後續狀態的影響,就可以同時利用到上述兩個啟發.而所有狀態的效用值總稱為效用跡(eligibility traces,ES).定義:

  • 圖片

  • 公式中的1()是一個真判斷表示式,表示當時取值為1,其餘條件下取值為0.

  • 下圖給出了效用E對於時間t的一個可能的曲線:

  • 圖片

  • 該圖橫座標是時間,橫座標下有豎線的位置代表當前時刻的狀態為s,縱座標是效用的值.可以看出當某一狀態連續出現時,E值會在一定衰減的基礎上有一個單位數值的提高,此時認為該狀態將對後續狀態的影響較大,如果該狀態很長時間沒有經歷,那麼該狀態的E值將逐漸趨向於0,表明該狀態對於較遠的後續狀態價值的影響越來越少.

  • 效用跡的提出是基於一個信度分配(Credit Assignment)問題的,打個比方,最後我們去跟別人下圍棋,最後輸了,那到底該中間我們下的哪一步負責?或者說,每一步對於最後我們輸掉比賽這個結果,分別承擔多少責任?這就是一個信度分配問題。對於小鼠問題,小鼠先聽到三次鈴聲,然後看見燈亮,接著就被電擊了,小鼠很生氣,它仔細想,究竟是鈴聲導致的它被電擊,還是燈亮導致的呢?如果按照事件的發生頻率來看,是鈴聲導致的,如果按照最近發生原則來看,那就是燈亮導致的,但是,更合理的想法是,這二者共同導致小鼠被電擊了,於是小鼠為這兩個事件分別分配了權重,如果某個事件發生,那麼 對應的效用跡的值就加1,如果在某一段時間未發生,則按照某個衰減因子進行衰減,這也就是上面的效用跡的計算公式了。

  • 需要指出的是,針對每一個狀態存在一個E值,且E值並不需要等到狀態序列到達終止狀態才能計算出來,它是根據已經經過的狀態序列來計算得到,並且在每一個時刻都對每一個狀態進行一次更新.E值存在飽和現象,有一個瞬時最高上限:圖片

  • E值是一個非常符合神經科學相關理論的,非常精巧的設計.可以把它看成是神經元的一個引數,它反映了神經元對某一刺激的敏感性和適應性.神經元在接受刺激時會有反饋,在持續刺激時反饋一般也比較強,當間歇一段時間不刺激的時候,神經元又逐漸趨向靜息狀態;同時不論如何增加刺激的頻率,神經元有一個最大飽和反饋.

  • 圖片

  • 後向視角使用了我們剛剛定義的效用跡,每個狀態都儲存了一個效用跡。我們可以將效用跡理解為一個權重,狀態被訪問的時間離現在越久遠,其對於值函式的影響就越小,狀態被訪問的次數越少,其對於值函式的影響也越小。同樣用一個例子說明:有個人坐在狀態流上,手裡拿著話筒,面朝著已經經歷過的狀態獲得當前回報並利用下一個狀態的值函式得到TD偏差之後,此人會向已經經歷過的狀態喊話告訴這些已經經歷過的狀態處的值函式需要利用當前時刻的TD偏差進行更新。此時過往的每個狀態值函式更新的大小應該跟距離當前狀態的步數有關。

  • 如果我們在更新狀態價值時把該狀態的效用同時考慮進來,那麼價值更新可以表示為:

  • 圖片

  • 當時,一直成立,此時價值更新等同於TD(0)演算法:

  • 圖片

  • 當 = 1時,在每完成一個狀態序列後更新狀態價值時,其完全等同於MC學習;但在引入了效用跡後,可以每經歷一個狀態就更新狀態的價值,這種實時更新的方法並不完全等同於MC.

  • 當時,在每完成一個狀態序列後更新價值時,基於前向認識的TD()與基於反向認識的TD()完全等效;不過在進行線上實時學習時,兩者存在一些差別.

程式設計實踐

  • 本章的程式設計實踐將使用MC學習來評估二十一點遊戲中一個玩家的策略。為了完成這個任務,我們需要先了解二十一點遊戲的規則,並構建一個遊戲場景讓莊家和玩家在一個給定的策略下進行博弈生成對局資料。這裡的對局資料在強化學習看來就是一個個完整的狀態序列組成的集合。然後我們使用本章介紹的蒙特卡羅演算法來評估其中玩家的策略。本節的難點不在於蒙特卡羅學習演算法的實現,而是對遊戲場景的實現並生成讓蒙特卡羅學演算法學習的多個狀態序列.

二十一點遊戲規則

  • 二十一點遊戲是一個比較經典的對弈遊戲,其規則也有各種不同的版本,為了簡化,本文僅介紹由一個莊家 (dealer) 和一個普通玩家 (player,下文簡稱玩家) 共2位遊戲者參與的一個比較基本的規則版本。遊戲使用一副除大小王以外的52張撲克牌,遊戲者的目標是使手中的牌的點數之和不超過21點且儘量大。其中2-10的數字牌點數就是牌面的數字,J,Q,K三類牌均記為10 點,A既可以記為1也可以記為11,由遊戲者根據目標自己決定。牌的花色對於計算點數沒有影響。

  • 開局時,莊家將依次連續發2張牌給玩家和莊家,其中莊家的第一張牌是明牌,其牌面資訊對玩家是開放的,莊家從第二張牌開始的其它牌的資訊不對玩家開放。玩家可以根據自己手中牌的點數決定是否繼續叫牌 (twist) 或停止叫牌 (stick), 玩家可以持續叫牌,但一旦手中牌點數超過 21 點則停止叫牌。當玩家停止叫牌後,莊家可以決定是否繼續叫牌。如果莊家停止叫牌,對局結束,雙方亮牌計算輸贏計算輸贏的規則如下:如果雙方點數均超過21點或雙方點數相同,則和局;一方21點另一方不是21點,則點數為21 點的遊戲者贏;如果雙方點數均不到21點,則點數離21點近的玩家獲勝

將二十一點遊戲建模為強化學習問題

  • 為了講解基於完整狀態序列的蒙特卡羅學習演算法,我們把二十一點遊戲建模成強化學習問題,設定由下面三個引數來集體描述一個狀態:莊家的明牌 (第一張牌) 點數;玩家手中所有牌點數之和;玩家手中是否還有“可用 (useable)”的 A(ace)。前兩個比較好理解,第三個引數是與玩家策略相關的,玩家是否有A這個比較好理解,可用的A指的是玩家手中的A按照目標最大化原則是否沒有被計作1點,如果這個A沒有被記為1點而是計為了11點,則成這個A為可用的A,否則認為沒有可用的A,當然如果玩家手中沒有A,那麼也被認為是沒有可用的A。例如玩家手中的牌為“A,3,6”,那麼此時根據目標最大化原則,A將被計為 11 點,總點數為20點,此時玩家手中的A稱為可用的A。加入玩家手中的牌為“A,5,7”,那麼此時的A不能被計為11點只能按1計,相應總點數被計為13點,否則總點數將為23點,這時的A就不能稱為可用的A

  • 根據我們對狀態的設定,我們使用由三個元素組成的元組來描述一個狀態。例如使用 (10,15,0) 表示的狀態是莊家的明牌是10,玩家手中的牌加起來點數是15,並且玩家手中沒有可用的A,(A,17,1) 表述的狀態是莊家第一張牌為A,玩家手中牌總點數為 17, 玩家手中有可用的A。這樣的狀態設定不考慮玩家手中的具體牌面資訊,也不記錄莊家除第一張牌外的其它牌資訊。所有可能的狀態構成了狀態空間.

  • 該問題的行為空間比較簡單,玩家只有兩種選擇:“繼續叫牌”或“停止叫牌”。

  • 該問題中的狀態如何轉換取決於遊戲者的行為以及後續發給遊戲者的牌,狀態間的轉移概率很難計算.

  • 可以設定獎勵如下:當棋局未結束時,任何狀態對應的獎勵為 0;當棋局結束時,如果玩家贏得對局,獎勵值為1,玩家輸掉對局,獎勵值為-1,和局是獎勵為0。

  • 本問題中衰減因子

  • 遊戲者在選擇行為時都會遵循一個策略。在本例中,莊家遵循的策略是隻要其手中的牌點數達到或超過17點就停止叫牌。我們設定玩家遵循的策略是隻要手中的牌點數不到20點就會繼續叫牌,點數達到或超過20點就停止叫牌.

  • 我們的任務是評估玩家的這個策略,即計算在該策略下的狀態價值函式,也就是計算狀態空間中的每一個狀態其對應的價值.

遊戲場景的搭建

  • 首先來搭建這個遊戲場景,實現生成對局資料的功能,我們要實現的功能包括:統計遊戲者手中牌的總點數、判斷當前牌局資訊對應的獎勵、實現莊家與玩家的策略、模擬對局的過程生成對局資料等。為了能儘可能生成較符合實際的對局資料,我們將迴圈使用一副牌,對局過程中發牌、洗牌、收集已使用牌等過程都將得到較為真實的模擬。我們使用物件導向的程式設計思想,通過構建遊戲者類和遊戲場景類來實現上述功能.

  • 首先匯入我們要使用的庫

from random import shuffle
from queue import  Queue
from tqdm import tqdm
import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits.mplot3d import Axes3D
from utils import str_key,set_dict,get_dict
  • 經過分析,一個單純的二十一點遊戲者應該至少能記住對局過程中手中牌的資訊,知道自己的行為空間,還應該能辨認單張牌的點數以及手中牌的總點數,為此遊戲者能夠接受發給他的牌以及一局結束後將手中的牌扔掉等.為此編寫了一個名為Gamer的遊戲者類.程式碼如下:

class Gamer():
    '''遊戲者'''
    def __init__(self, name = "", A = None, display = False):
        self.name = name
        self.cards = [] #手中的牌
        self.display = display #是否顯示對局文字資訊
        self.policy = None #策略
        self.lerning_method = None #學習方法
        self.A = A #行為空間

    def __str__(self):
        return self.name

    def _value_of(self, card):
        '''
        根據牌的字元判斷牌的數值大小,A被輸出為1,JQK均為10,其餘按牌字元對應的數字取值
        :param card: 牌面資訊str
        :return: 牌的大小數值int, A返回1
        '''
        try:
            v = int(card)
        except:
            if card == "A":
                v = 1
            elif card in ['J','Q','K']:
                v = 10
            else:
                v = 0
        finally:
            return v

    def get_points(self):
        '''
        統計一手牌分值,如果使用了A的1點,同時返回True
        :return:tuple(返回牌的總點數,是否使用了可複用的Ace)
        例如['A','10','3'] 返回 (14,False)
        ['A','10'] 返回(21, True)
        '''
        num_of_uneable_ace = 0 #預設沒有拿到Ace
        total_point = 0 #總值
        cards = self.cards
        if cards is None:
            return 0, False
        for card in cards:
            v = self._value_of(card)
            if v == 1:
                num_of_uneable_ace += 1
                v = 11
            total_point += v
        while total_point > 21 and num_of_uneable_ace > 0:
            total_point -= 10
            num_of_uneable_ace -= 1
        return total_point,bool(num_of_uneable_ace)

    def receive(self, cards = []):
        '''
        玩家獲得一張或者多張牌
        :param cards: 玩家獲得的牌
        :return: None
        '''
        cards = list(cards)
        for card in cards:
            self.cards.append(card)

    def discharge_cards(self):
        '''
        玩家把手中的牌清空,扔牌
        :return: None
        '''
        self.cards.clear()

    def cards_info(self):
        '''
        展示牌面具體資訊
        :return: None
        '''
        self._info("{}{}現在的牌:{}\n".format(self.role,self,self.cards))
    def _info(self,msg):
        if self.display:
            print(msg, end="")
  • 在程式碼中,構造一個遊戲者可以提供三個引數,分別是該遊戲者的姓名 (name),行為空間 (A) 和是否在終端顯示具體資訊 (display)。其中設定第三個引數主要是由於除錯和展示的需要,我們希望一方面遊戲在生成大量對局資訊時不要輸出每一局的細節,另一方面在觀察細節時希望能在終端給出某時刻莊家和玩家手中具體牌的資訊以及他們的行為等。我們還給遊戲者增加了一些輔助屬性,比如遊戲者姓名、策略、學習方法等,還設定了一個display以及一些顯示資訊的方法用來在對局中在終端輸出對局資訊。在計算單張牌面點數的時候,借用了異常處理。在統計一手牌的點數時,要考慮到可能出現多張 A 的情況。讀者可以輸入一些測試牌的資訊觀察這兩個方法的輸出。

  • 在二十一點遊戲中,莊家和玩家都是一個遊戲者,我們可以從Gamer類繼承出Dealer類和Player類分別表示莊家和普通玩家。莊家和普通玩家的區別在於兩者的角色不同、使用的策略不同。其中莊家使用固定的策略,他還能顯示第一張明牌給其他玩家。在本章程式設計實踐中,玩家則使用最基本的策略,由於我們的玩家還要進行基於蒙特卡羅演算法的策略評估,他還需要具備構建一個狀態的能力。我們擴充套件的莊家類如下:

class Dealer(Gamer):
    '''
    莊家
    '''
    def __init__(self, name = "", A = None, display = False):
        super(Dealer,self).__init__(name, A, display)
        self.role = "莊家" #角色
        self.policy = self.dealer_policy #莊家策略

    def first_card_value(self):
        '''
        顯示第一張明牌
        :return: 明牌的點數
        '''
        if self.cards is None or len(self.cards) == 0:
            return 0
        return self._value_of(self.cards[0])

    def dealer_policy(self, Dealer = None):
        '''
        莊家策略的細節
        :param Dealer:莊家
        :return:莊家的策略(行為)
        '''
        action = ""
        dealer_points, _ = self.get_points()
        if dealer_points >= 17:
            action = self.A[1] #停止叫牌
        else:
            action = self.A[0] #繼續叫牌
        return action
  • 在莊家類的構造方法中宣告其基類是遊戲者 (Gamer),這樣他就具備了遊戲者的所有屬性和方法了。我們給莊家貼了個“莊家”的角色標籤,同時指定了其策略,在具體的策略方法中,規定莊家的牌只要達到或超過 17 點就不再繼續叫牌。玩家類的程式碼如下:

class Player(Gamer):
    '''
    玩家
    '''
    def __init__(self, name = "", A = None, display = False):
        super(Player, self).__init__(name, A, display)
        self.policy = self.native_policy
        self.role = "玩家"

    def get_state(self, dealer):
        dealer_first_card_value = dealer.first_card_value()
        player_points, useable_ace = self.get_points()
        return dealer_first_card_value, player_points, useable_ace

    def get_state_name(self, dealer):
        return str_key(self.get_state(dealer))

    def native_policy(self, dealer = None):
        player_points, _ = self.get_points()
        if player_points < 20:
            action = self.A[0]
        else:
            action = self.A[1]
        return action
  • 類似的,我們的玩家類也繼承子游戲者 (Gamer),指定其策略為最原始的策略 (naive_policy),規定玩家只要點數小於 20 點就會繼續叫牌。玩家同時還會根據當前局面資訊得到當前局面的狀態,為策略評估做準備.

  • 至此遊戲者這部分的建模工作就完成了,接下來將準備遊戲桌、遊戲牌、組織遊戲對局、判定輸贏等功能。我們把所有的這些功能包裝在一個名稱為 Arena 的類中。Arena 類的構造方法如下:

class Arena():
    '''
    負責遊戲管理
    '''
    def __init__(self, display = None, A = None):
        self.cards = ['A','2','3','4','5','6','7','8','9','10','J','Q',"K"] * 4
        self.card_q = Queue(maxsize=52) #發牌器,裡面是洗好的牌
        self.cards_in_pool = [] #已經用過的公開的牌
        self.display = display
        self.episodes = [] #產生的對局資訊列表
        self.load_cards(self.cards) #把初始狀態的52張牌裝入發牌器
        self.A = A #獲得行為空間
  • Arena 類接受兩個引數,這兩個引數與構建遊戲者的引數一樣。Arena 包含的屬性有:一副不包括大小王、花色資訊的牌 (cards)、一個裝載洗好了的牌的發牌器 (cards_q),一個負責收集已經使用過的廢牌的池子 (cards_in_pool),一個記錄了對局資訊的列表 (episodes),還包括是否顯示具體資訊以及遊戲的行為空間等。在構造一個 Arena 物件時,我們同時把一副新牌洗好並裝進了發牌器,這個工作在 load_cards 方法裡完成。我們來看看這個方法的細節:

    def load_cards(self, cards):
        '''
        把收集的牌洗一洗,重新裝到發牌器中
        :param cards: 要裝入發牌器的多張牌list
        :return: None
        '''
        shuffle(cards) #洗牌
        for card in cards: #deque資料結構只能一個一個新增
            self.card_q.put(card)
        cards.clear()#把原來的牌清空
        return
  • 這個方法接受一個引數 (cards),多數時候我們將 cards_in_pool 傳給這個方法,也就是把桌面上已使用的廢牌收集起來傳給這個方法,該方法將首先把這些牌的次序打亂,模擬洗牌操作。隨後將洗好的牌放入發牌器。完成洗牌裝牌功能。Arena 應具備根據莊家和玩家手中的牌的資訊判斷當前誰贏誰輸的能力,該能力通過如下的方法 (reward_of) 來實現:

    def reward_of(self, dealer, player):
        '''
        判斷玩家獎勵值,附帶玩家,莊家的牌點資訊
        :param dealer: 莊家
        :param player:玩家
        :return: tuple 獎勵值 玩家點數 莊家點數 是否使用A
        '''
        dealer_points, _ = dealer.get_points()
        player_points, useable_ace = player.get_points()
        if player_points > 21:
            reward = -1
        else:
            if player_points > dealer_points or dealer_points > 21:
                reward = 1
            elif player_points == dealer_points:
                reward = 0
            else:
                reward = -1
        return reward, player_points, dealer_points, useable_ace
  • 該方法接受莊家和玩家為引數,計算對局過程中以及對局結束時牌局的輸贏資訊 (reward)後,同時還返回當前玩家、莊家具體的總點數以及玩家是否有可用的 A 等資訊。

  • 下面的方法實現了 Arena 物件如何向莊家或玩家發牌的功能:

    def serve_card_to(self, player, n=1):
        '''
        給莊家或玩家發牌,如果牌不夠則將公開牌池的牌洗一洗重新發牌
        :param player: 一個莊家或玩家
        :param n: 一次連續發牌的數量
        :return:None
        '''
        cards = [] #將要發出的牌
        for _ in range(n):
            #要考慮發牌器沒有牌的情況
            if self.card_q.empty():
                self._info("\n發牌器沒牌了,整理廢牌,重新發牌;")
                shuffle(self.cards_in_pool)
                self._info("一共整理了{}張已用牌, 重新放入發牌器\n".format(len(self.cards_in_pool)))
                assert (len(self.cards_in_pool) > 20)
                # 確保一次能收集較多的牌
                # 程式碼編寫不合理時,可能會出現即使某一玩家爆點了也還持續地叫牌,會導致玩家手中的牌變多而發牌器和
                # 已使用的牌都很少,需避免這種情況
                self.load_cards(self.cards_in_pool) # 將收集來的用過的牌洗好送入發牌器重新使用
            cards.append(self.card_q.get()) # 從發牌器發出一張牌
        self._info("發了{}張牌({})給{}{}".format(n, cards, player.role,player))
        player.receive(cards) #某玩家接受發出的牌
        player.cards_info()
    def _info(self, message):
        if self.display:
            print(message, end = "")
  • 這個方法 (serve_card_to) 接受一個玩家 (player) 和一個整數 (n) 作為引數,表示向該玩家一次發出一定數量的牌,在發牌時如果遇到發牌器裡沒有牌的情況時會將已使用的牌收集起來洗好後送入發牌器,隨後在把需要數量的牌發給某一玩家。程式碼中的方法 (_info) 負責根據條件在終端輸出對局資訊.

  • 當一局結束時,Arena 物件還負責把玩家手中的牌回收至已使用的廢牌區,這個功能由下面這個方法來完成:

    def recycle_cards(self, *players):
        '''
        回收玩家手中的牌到公開使用過的牌池中
        :param players: 一個玩家或莊家
        :return:None
        '''
        if len(players) == 0:
            return
        for player in players:
            for card in player.cards:
                self.cards_in_pool.append(card)
            player.discharge_cards()#玩家不再留有這些牌
  • 剩下一個最關鍵的功能就是,如何讓莊家和玩家進行一次對局,編寫下面的方法來實現這個功能:

    def play_game(self, dealer, player):
        '''
        玩一局21點,生成一個狀態序列以及最終獎勵(中間獎勵為0)
        :param dealer: 莊家
        :param player: 玩家
        :return: tuple: episode, reward
        '''
        self._info("=============開始新一局=============\n")
        self.serve_card_to(player, n=2) #發兩張牌給玩家
        self.serve_card_to(dealer, n=2) #發兩張牌給莊家
        episode = [] #記錄一個對局資訊
        if player.policy is None:
            self._info("玩家需要一個策略")
            return
        if dealer.policy is None:
            self._info("莊家需要一個策略")
            return
        while True:
            action = player.policy(dealer)
            # 玩家的策略產生一個行為
            self._info("{}{}選擇:{};".format(player.role, player, action))
            episode.append((player.get_state_name(dealer), action)) #記錄一個(s,a)
            if action == self.A[0]: #繼續叫牌
                self.serve_card_to(player) # 發一張牌給玩家
            else: #停止叫牌
                break
        #玩家停止叫牌後要計算下玩家手中的點數,玩家如果爆了,莊家就不用繼續了
        reward, player_points, dealer_points, useable_ace = self.reward_of(dealer, player)
        if player_points > 21:
            self._info("玩家爆點{}輸了,得分:{}\n".format(player_points,reward))
            self.recycle_cards(player, dealer)
            self.episodes.append((episode, reward)) #預測的時候需要行為episode list後集中學習V
            # 在蒙特卡洛控制的時候, 可以不需要episodes list,生成一個episode學習一個,下同
            self._info("===============本局結束===============")
            return episode, reward
        #玩家並沒有超過21點
        self._info("\n")
        while True:
            action = dealer.policy() #莊家從其策略中獲取一個行為
            self._info("{}{}選擇:{};".format(dealer.role,dealer,action))
            #狀態只記錄莊家第一張牌資訊,此時玩家不再叫牌,(s,a)不必重複記錄
            if action == self.A[0]: #莊家繼續叫牌
                self.serve_card_to(dealer)
            else:
                break;
        #雙方均停止叫牌了
        self._info("\n雙方均停止叫牌")
        reward, player_points, dealer_points,useable_ace = self.reward_of(dealer,player)
        player.cards_info()
        dealer.cards_info()
        if reward == +1:
            self._info("玩家贏了!")
        elif reward == -1:
            self._info("玩家輸了!")
        else:
            self._info("雙方和局!")
        self._info("玩家{}點,莊家{}點\n".format(player_points, dealer_points))
        self._info("=================本局結束==================")
        self.recycle_cards(player, dealer) #回收玩家和莊家手中的牌至公開牌池
        self.episodes.append((episode, reward)) #將剛才產生的完整對局新增值狀態序列列表,蒙特卡洛控制不需要
        return episode, reward
  • 這段程式碼雖然比較長,但裡面包含許多反映對局過程的資訊,使得程式碼也比較容易理解。該方法接受一個莊家一個玩家為引數,產生一次對局,並返回該對局的詳細資訊。需要指出的是玩家的策略要做到在玩家手中的牌超過 21 點時強制停止叫牌。其次在玩家停止叫牌後,Arena 對局面進行一次判斷,如果玩家超過 21 點則本局結束,否則提示莊家選擇行為。當莊家停止叫牌後後,Arena 對局面再次進行以此判斷,結束對局並將該對局產生的詳細資訊記錄一個 episode物件,並附加地把包含了該局資訊的 episode 物件聯合該局的最終輸贏 (獎勵) 登記至 Arena 的成員屬性 episodes中.

  • 有了生成一次對局的方法,我們編寫下面的程式碼來一次性生成多個對局:

    def play_games(self, dealer, player, num = 2, show_statistic = True):
        '''
        一次性玩多局遊戲
        :param dealer:
        :param player:
        :param num:
        :param show_statistic:
        :return:
        '''
        results = [0, 0, 0] #玩家負, 和, 勝局數
        self.episodes.clear()
        for i in tqdm(range(num)):
            episode, reward = self.play_game(dealer, player)
            results[1 + reward] += 1
            if player.lerning_method is not None:
                player.lerning_method(episode, reward)
            if show_statistic:
                print("共玩了{}局,玩家贏{}局,和{}局,輸{}局,勝率:{:.2f},不輸率:{:.2f}"\
                      .format(num, results[2], results[1], results[0], results[2]/num,\
                      (results[2] + results[1]) / num))
        return
    def _info(self, message):
        if self.display:
            print(message, end="")
  • 該方法接受一個莊家、一個玩家、需要產生的對局數量、以及是否顯示多個對局的統計資訊這四個引數,生成指定數量的對局資訊,這些資訊都儲存在 Arena 的 episodes 物件中。為了相容具備學習能力的玩家,我們設定了在每一個對局結束後,如果玩家能夠從中學習,則提供玩家一次學習的機會,在本章中的玩家不具備從對局中學習改善策略的能力,這部分內容將在下一章詳細講解。如果引數設定為顯示統計資訊,則會在指定數量的對局結束後顯示一共對局多少,玩家的勝率等.

生成對局資料

  • 下面的程式碼將生成一個莊家,一個玩家,一個Arena物件,並進行20萬次的對局:

A = ["繼續叫牌","停止叫牌"]
display = False
#建立一個玩家一個莊家,玩家使用原始策略,莊家使用其固定的策略
player = Player(A = A, display = display)
dealer = Dealer(A = A, display = display)
#建立一個場景
arena = Arena(A = A, display = display)
#生成num個完整的對局
arena.play_games(dealer,player,num=200000)
# 共玩了200000局,玩家贏58564局,和11369局,輸130067局,勝率:0.29,不輸率:0.35
# 100%|██████████| 200000/200000 [00:17<00:00, 11140.95it/s]

策略評估

  • 對局生成的資料均儲存在物件 arena.episodes 中,接下來的工作就是使用這些資料來對player的策略進行評估,下面的程式碼完成這部分功能:

def policy_evaluate(episodes, V, Ns):
    '''
    統計每個狀態的價值,衰減因子為1,中間狀態的即時獎勵為0,遞增式蒙特卡洛評估
    :param episodes: 狀態序列
    :param V:狀態價值字典
    :param Ns:狀態被訪問的次數節點
    :return:
    '''
    for episode, r in episodes:
        for s, a in episode:
            ns = get_dict(Ns, s)
            v = get_dict(V, s)
            set_dict(Ns, ns+1, s)
            set_dict(V, v+(r-v)/(ns+1), s)

V = {} #狀態價值字典
Ns = {}#狀態被訪問的次數節點
policy_evaluate(arena.episodes, V, Ns) #學習V值
  • 其中,V 和 Ns 儲存著蒙特卡羅策略評估程式中的價值和統計次數資料,我們使用的是每次訪問計數的方法。我們還可以編寫如下的方法將價值函式繪製出來:

def draw_value(value_dict, useable_ace = 0, is_q_dict = False, A = None):
    # 定義figure
    fig = plt.figure()
    # 將figure變為3d
    ax = Axes3D(fig)
    # 定義x, y
    x = np.arange(1, 11, 1) # 莊家第一張牌
    y = np.arange(12, 22, 1) # 玩家總分數
    # 生成網格資料
    X, Y = np.meshgrid(x, y)
    # 從V字典檢索Z軸的高度
    row, col = X.shape
    Z = np.zeros((row,col))
    if is_q_dict:
        n = len(A)
    for i in range(row):
        for j in range(col):
            state_name = str(X[i,j])+"_"+str(Y[i,j])+"_"+str(useable_ace)
            if not is_q_dict:
                Z[i,j] = get_dict(value_dict, state_name)
            else:
                assert(A is not None)
                for a in A:
                    new_state_name = state_name + "_" + str(a)
                    q = get_dict(value_dict, new_state_name)
                    if q >= Z[i,j]:
                        Z[i,j] = q
    # 繪製3D曲面
    ax.plot_surface(X, Y, Z, rstride = 1, cstride = 1, cmap = plt.cm.cool)
    plt.show()

draw_value(V, useable_ace=True, A = A)#繪製有可用的A時狀態價值圖
draw_value(V, useable_ace=False, A = A)#繪製無可用的A時狀態價值圖
  • 結果如下,第一個圖是有可用的ace的價值函式圖,第二個是沒有可用的ace的價值函式圖:

  • 圖片圖片

  • 我們可以設定各物件display的值為True,來生成少量對局並輸出對局詳細資訊:

display = True
player.display, dealer.display, arena.display = display,display,display
arena.play_games(dealer,player, num=2)
# =============開始新一局=============
# 發了2張牌(['2', 'Q'])給玩家玩家現在的牌:['2', 'Q']
# 發了2張牌(['J', 'A'])給莊家莊家現在的牌:['J', 'A']
# 玩家選擇:繼續叫牌;發了1張牌(['10'])給玩家玩家現在的牌:['2', 'Q', '10']
# 玩家選擇:停止叫牌;玩家爆點22輸了,得分:-1
# ===============本局結束===============共玩了2局,玩家贏0局,和0局,輸1局,勝率:0.00,不輸率:0.00
# =============開始新一局=============
# 發了2張牌(['5', 'Q'])給玩家玩家現在的牌:['5', 'Q']
# 發了2張牌(['5', '5'])給莊家莊家現在的牌:['5', '5']
# 玩家選擇:繼續叫牌;發了1張牌(['9'])給玩家玩家現在的牌:['5', 'Q', '9']
# 玩家選擇:停止叫牌;玩家爆點24輸了,得分:-1
# ===============本局結束===============共玩了2局,玩家贏0局,和0局,輸2局,勝率:0.00,不輸率:0.00
  • 本節程式設計實踐中,我們構建了遊戲者基類並擴充套件形成了莊家類和玩家類來模擬玩家的行為,同時構建了遊戲場景類來負責進行對局管理。在此基礎上使用蒙特卡羅演算法對遊戲中玩家的原始策略進行了評估。在策略評估環節,我們並沒有把價值函式 (字典)、計數函式 (字典) 以及策略評估方法設計為玩家類的成員物件和成員方法,這只是為了講解的方便,讀者完全可以將它們設計為玩家的成員變數和方法。下一章的程式設計實踐中,我們將繼續通過二十一點遊戲介紹如何使用蒙特卡羅控制尋找最優策略,本節建立的 Dealer, Player 和 Arena 類將得到複用和擴充套件.

相關文章