iT邦幫忙

2024 iThome 鐵人賽

DAY 7
1

在日本趕稿趕到快發瘋,如果有哪裡解釋不清楚的歡迎留言跟我說。
hackMD原稿

昨天分享了如果是較為複雜的遊戲,可以使用審局函數來限制Minimax的搜索深度,好讓我們的程式停下來。但是搜索深度通常會直接引響到程式的棋力,搜索得愈深通常也會愈強,要想在相同時間內搜索得更深,那就得升級硬體設備了,沒有什麼是花錢解決不了的。

什麼?你說升級太貴了?

image

以上純屬開玩笑,這是我的缺點。
身為軟體人當然要用寫程式解決,今天要使用Alpha-Beta Pruning演算法來優化我們的程式。

Alpha-Beta Pruning

Alpha-Beta Pruning(Alpha-Beta剪枝)是一種搜索演算法,為了改進Minimax Algorithm而產生的,用來減少Minimax產生的對局樹節點數。在很多時候Minimax對局樹展開是相當費時的,所以我們應該要盡可能的減少不必要的節點展開。當演算法計算出某節點的後續走法比之前節點的還差時,就會停止計算該節點的後續子節點。這樣可以省去搜索那些沒有機會的節點,把搜索時間用在更有希望的子樹上,提升單位時間的搜索深度。

不要跟他拼硬體,嘗試切他節點。

image

Alpha-Beta Pruning在原本的Minimax Algorithm新增加了兩個參數,α跟β,α記錄max層的目前的最大值,β記錄min層目前的最小值。兩個參數以交錯的方式向下層傳遞,當我們在max層取最大值的時候發現了一個大於等於β的值,就不用再對其他分支進行搜索,此剪枝稱為β cut。當我們在min層取最小值的時候發現了一個小於等於α的值,一樣也不用再對其他分支進行搜索,此剪枝稱為α cut。

以下圖為例,此圖為一個深度優先由左至右拜訪的對局樹。

測試minimax

當搜索至D節點時,更新C節點的值為4,小於此時的α值5,發生α cut。此時C節點若再繼續往其它子節點搜尋,C節點的值也只會小於等於4,位於max層的A節點會選擇最大的子節點B節點。
所以不管結果如何,C節點的結果都不會改變A節點的值了。此時我們就可以把E節點給剪掉,C節點剩下的子節點都可以不必再搜索了。
當搜索至I節點時,更新H節點的值為6,小於等於此時的α值6,發生α cut,所以一樣把J節點給剪掉不必再搜了。
當M節點更新為8時,8大於等於此時的β值3,發生β cut,所以將M節點剩下的子節點都剪掉。

測試ab

實作

這邊比起昨天就只是需要多去維護alphabeta兩個參數,程式寫起來也非常簡單,幾乎沒有什麼改變。

def alpha_beta_pruning(board, depth, current_player, maximizing_player, alpha, beta):
    """
    board: 棋盤狀態
    depth: 目前遞迴深度
    current_player: 當前回合玩家 ('X' 或 'O')
    maximizing_player: 最大化玩家 ('X' 或 'O')
    alpha: 紀錄max層的下限值
    beta: 紀錄min層的上限值
    """
    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, alpha, beta) 
            board.undo_move(move)
            best_score = max(score, best_score)
            alpha = max(alpha, best_score)  # 更新 alpha
            if beta <= alpha:  # Beta 剪枝
                break
        return 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, alpha, beta)
            board.undo_move(move)
            best_score = min(score, best_score)
            beta = min(beta, best_score)  # 更新 beta
            if beta <= alpha:  # Alpha 剪枝
                break
        return best_score

如果是井字遊戲的話那就更簡單了,甚至不需要使用alphabeta做為參數傳遞下去,因為他的狀態很單純就是只有1、0、-1。
我們只需要找到一種勝利的方式,不用找出全部,在Max層中只要找到其中一個子節點能獲勝,就可以直接break不再繼續搜索其他分支了,反之亦然。

def alpha_beta_pruning(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 = alpha_beta_pruning(board, depth + 1, opponent, maximizing_player)
            board.undo_move(move)
            if score == 1:
                break
            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 = alpha_beta_pruning(board, depth + 1, opponent, maximizing_player)
            board.undo_move(move)
            if score == -1:
                break
            best_score = min(score, best_score)
    return best_score

Negamax + Alpha-Beta Pruning

如果是Negamax的版本一樣可以使用Alpha-Beta Pruning,這邊只需要注意alphabeta也要跟著做交換。

def negamax(board, depth, player, maximizing_player, alpha, beta):
    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)

    best_score = -float('inf')
    opponent = 'O' if player == 'X' else 'X'
    
    for move in board.get_available_moves():
        board.set_move(move, player)
        score = -negamax(board, depth + 1, opponent, maximizing_player, -beta, -alpha)
        board.undo_move(move)
        best_score = max(score, best_score)

    return best_score

結論

使用Alpha-Beta Pruning可以提早剪去較差的分支,剪掉的分支愈多,單位時間能搜索的深度就能夠愈深,後續幾天會再分享更多種的剪枝與加速方式。


上一篇
Day6 Evaluation Function
下一篇
Day8 電腦對局競賽
系列文
猴子也能懂的電腦對局 : 30天打造自己的對局AI30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言