Day6 Evaluation Function
===
在有限的時間中,很多遊戲是不可能搜索完所有的結果的,因為昨天我們的範例是井字遊戲,他的對局樹複雜度很低,可以直接把所有下法都試一遍,如果換成圍棋、西洋棋等,就不可能這麼做了,你會以為你的程式進入到了無窮迴圈之中,根本跑不完,所以我們必須想個辦法讓他停下來,這時候就可以使用審局函數。
## 審局函數
審局函數(Evaluation Function)是電腦對局中用來評估當前盤面好壞的一種數學函數。其主要目的是在沒有明確勝負的情況下,根據當前的盤面,給出一個評分,來衡量該局面對於某一方玩家的優劣程度。
判斷局勢是在各種對局中都相當重要的,你必須了解到目前的局面形勢才有辦法去決定你的策略,比如在優勢中可能會選擇較為保守的策略來確保最後能夠獲勝,在劣勢中則可能會考慮選擇激進的策略以求逆轉戰局。
如果不小心誤判了局勢,比如在劣勢中仍然選擇了保守的策略,則有可能得到「安樂死」的下場。
在我們限制搜索深度後,葉節點可能並沒有算到勝負結果,這時候就使用審局函數給予評分,然後一樣使用minimax一路回傳到根節點,如下圖所示:
![](https://hackmd.io/_uploads/By7zTxGTR.png)
### 完美審局函數
理論上任何一個遊戲都有一個完美的審局函數,假設 $f(x)$ 為井字遊戲的完美審局函數,$x$ 為任意盤面,只要輸入任意盤面就可以得到勝負結果。
獲勝:$f(x) = 1$
和棋:$f(x) = 0$
失敗:$f(x) = -1$
如果找到一個完美的審局函數,那遊戲等於被破解了,任何盤面只要經過此完美審局函數則立刻可以得知勝負。
### 近似審局函數
大多數情況我們找不到完美的審局函數,只能盡量找出一個近似解或是找出一個較佳的範圍。
那要如何設計審局函數呢?
根據不同遊戲我們必須設計不同的策略,以下是我認為比較常見的三大類做法:
1. 靜態評估
對當前盤面做出的靜態分析。
像是Day4中提到的人類經驗法則,此時就可以作為一種評估的方式,還可以根據**棋子數量、功能性、形狀特徵**等方式給分數,例如西洋棋、象棋在過去就很常透過子力分析來作為一種評估方式。
或是根據不同類型的遊戲可以使用特別的演算法,例如之前我與一位UCLA博士生Steven大大,曾經用影像處理中常見的**Morphology**(形態學)來實作圍棋的形勢判斷,如果過幾天我還能堅持下去的話會再來分享的。
2. 模擬法
**通過大量模擬對局,統計勝負結果來評估當前盤面。**
從當前盤面模擬下到結束,假設100次中贏了70次,那此盤面的勝率就評估為70%,最常見的方式就是使用Monte Carlo Method做大量的隨機模擬,這個後續章節也會更詳細的做介紹。
3. 機器學習
這邊最經典的例子就是AlphaGo中的value network,他就是用來評估當前盤面好壞的。
## 程式修改
我們在昨天的minimax中加上一個`depth`參數,用來讓遞迴停止,可以根據硬體設備或比賽規範的時間來設定搜索深度,當到達指定深度後,直接停止並且對當前盤面進行判斷。
```python=
def minimax(board, depth, current_player, maximizing_player):
"""
board: 棋盤狀態
depth: 目前遞迴深度
current_player: 當前回合玩家 ('X' 或 'O')
maximizing_player: 最大化玩家 ('X' 或 'O')
"""
winner = board.check_winner()
if winner is not None:
if winner == maximizing_player:
return 1
elif winner == 'Draw':
return 0
else:
return -1
if depth == 10:
return evaluate(board)
opponent = 'O' if current_player == 'X' else 'X'
if current_player == maximizing_player: # max層
best_score = -float('inf')
for move in board.get_available_moves():
board.set_move(move, current_player)
score = minimax(board, depth + 1, opponent, maximizing_player)
board.undo_move(move)
best_score = max(score, best_score)
else: # min層
best_score = float('inf')
for move in board.get_available_moves():
board.set_move(move, current_player)
score = minimax(board, depth + 1, opponent, maximizing_player)
board.undo_move(move)
best_score = min(score, best_score)
return best_score
```
我們要在其中多加入一個遞迴終止條件,比如此處我們設定為10,並且在每次遞迴呼叫的時候將`depth`+1。
```python=
if depth == 10:
return evaluate(board)
```
這邊的`evaluate`就是我們要設計的審局函數了,因為井字遊戲實在是沒什麼好設計的,所以這裡就先寫到這裡,其他遊戲的審局函數設計我們就保留到後面再來分享。
## 相關論文分享
其實研究各遊戲審局函數的論文也不少,最後來分享幾篇論文吧。
* [暗棋子力價值之審局設計](https://ndltd.ncl.edu.tw/cgi-bin/gs32/gsweb.cgi/login?o=dnclcdr&s=id=%22110NTPU0392012%22.&searchmode=basic)
* [六子棋之棋型分類及審局函數之研究](https://ndltd.ncl.edu.tw/cgi-bin/gs32/gsweb.cgi/login?o=dnclcdr&s=id=%22099NTNU5392014%22.&searchmode=basic)
* [暗棋殘局庫對審局函數之改良](https://ndltd.ncl.edu.tw/cgi-bin/gs32/gsweb.cgi/login?o=dnclcdr&s=id=%22112NTPU0392002%22.&searchmode=basic)
* [電腦象棋審局評分自動調整系統](https://www.airitilibrary.com/Article/Detail/U0030-0309201012110423)