python實現的基於蒙特卡洛樹搜索(MCTS)與UCT RAVE的五子棋游戲


更新

  • 2017.2.23有更新,見文末。

MCTS與UCT

下面的內容引用自徐心和與徐長明的論文《計算機博弈原理與方法學概述》:

蒙特卡洛模擬對局就是從某一棋局出發,隨機走棋。有人形象地比喻,讓兩個傻子下棋,他們只懂得棋規,不懂得策略,最終總是可以決出勝負。這個勝負是有偶然性的。但是如果讓成千上萬對傻子下這盤棋,那么結果的統計還是可以給出該棋局的固有勝率和勝率最高的着法。
蒙特卡洛樹搜索通過迭代來一步步地擴展博弈樹的規模,UCT 樹是不對稱生長的,其生長順序也是不能預知的。它是根據子節點的性能指標導引擴展的方向,這一性能指標便是 UCB 值。它表示在搜索過程中既要充分利用已有的知識,給勝率高的節點更多的機會,又要考慮探索那些暫時勝率不高的兄弟節點,這種對於“利用”(Exploitation)和“探索”(Exploration)進行權衡的關系便體現在 UCT 着法選擇函數的定義上,即子節點\(N_{i}\) 的 UCB 值按如下公式計算:

\[\frac{W_{i}}{N_{i}} + \sqrt{\frac{C \times lnN}{N_{i}}} \]

其中:
\(W_{i}\):子節點獲勝的次數;
\(N_{i}\):子節點參與模擬的次數;
\(N\):當前節點參與模擬的次數
\(C\):加權系數。
可見 UCB 公式由兩部分組成,其中前一部分就是對已有知識的利用,而后一部分則是對未充分模擬節點的探索。C小偏重利用;而 C大則重視探索。需要通過實驗設定參數來控制訪問節點的次數和擴展節點的閾值。

后面可以看到,在實際編寫代碼時,當前節點指的並不是具體的着法,而是當前整個棋局,其子節點才是具體的着法,它勢必參與了每個子節點所參與的模擬,所以N就等於其所有子節點參與模擬的次數之和。當C取1.96時,置信區間的置信度達到95%,也是實際選擇的值。

蒙特卡洛樹搜索(MCTS)僅展開根據 UCB 公式所計算過的節點,並且會采用一種自動的方式對性能指標好的節點進行更多的搜索。具體步驟概括如下:
1.由當前局面建立根節點,生成根節點的全部子節點,分別進行模擬對局;
2.從根節點開始,進行最佳優先搜索;
3.利用 UCB 公式計算每個子節點的 UCB 值,選擇最大值的子節點;
4.若此節點不是葉節點,則以此節點作為根節點,重復 2;
5.直到遇到葉節點,如果葉節點未曾經被模擬對局過,對這個葉節點模擬對局;否則為這個葉節點隨機生成子節點,並進行模擬對局;
6.將模擬對局的收益(一般勝為 1 負為 0)按對應顏色更新該節點及各級祖先節點,同時增加該節點以上所有節點的訪問次數;
7.回到 2,除非此輪搜索時間結束或者達到預設循環次數;
8.從當前局面的子節點中挑選平均收益最高的給出最佳着法。
由此可見 UCT 算法就是在設定的時間內不斷完成從根節點按照 UCB 的指引最終走到某一個葉節點的過程。而算法的基本流程包括了選擇好的分支(Selection)、在葉子節點上擴展一層(Expansion)、模擬對局(Simulation)和結果回饋(Backpropagation)這樣四個部分。
UCT 樹搜索還有一個顯著優點就是可以隨時結束搜索並返回結果,在每一時刻,對 UCT 樹來說都有一個相對最優的結果。

代碼實現

Board 類

Board類用於存儲當前棋盤的狀態,它實際上也是MCTS算法的根節點。

class Board(object):
    """
    board for game
    """

    def __init__(self, width=8, height=8, n_in_row=5):
        self.width = width
        self.height = height 
        self.states = {} # 記錄當前棋盤的狀態,鍵是位置,值是棋子,這里用玩家來表示棋子類型
        self.n_in_row = n_in_row # 表示幾個相同的棋子連成一線算作勝利

    def init_board(self):
        if self.width < self.n_in_row or self.height < self.n_in_row:
            raise Exception('board width and height can not less than %d' % self.n_in_row) # 棋盤不能過小

        self.availables = list(range(self.width * self.height)) # 表示棋盤上所有合法的位置,這里簡單的認為空的位置即合法

        for m in self.availables:
            self.states[m] = -1 # -1表示當前位置為空

  def move_to_location(self, move):
        h = move  // self.width
        w = move  %  self.width
        return [h, w]

    def location_to_move(self, location):
        if(len(location) != 2):
            return -1
        h = location[0]
        w = location[1]
        move = h * self.width + w
        if(move not in range(self.width * self.height)):
            return -1
        return move

    def update(self, player, move): # player在move處落子,更新棋盤
        self.states[move] = player
        self.availables.remove(move)

MCTS 類

核心類,用於實現基於UCB的MCTS算法。

class MCTS(object):
    """
    AI player, use Monte Carlo Tree Search with UCB
    """

    def __init__(self, board, play_turn, n_in_row=5, time=5, max_actions=1000):

        self.board = board
        self.play_turn = play_turn # 出手順序
        self.calculation_time = float(time) # 最大運算時間
        self.max_actions = max_actions # 每次模擬對局最多進行的步數
        self.n_in_row = n_in_row

        self.player = play_turn[0] # 輪到電腦出手,所以出手順序中第一個總是電腦
        self.confident = 1.96 # UCB中的常數
        self.plays = {} # 記錄着法參與模擬的次數,鍵形如(player, move),即(玩家,落子)
        self.wins = {} # 記錄着法獲勝的次數
        self.max_depth = 1

    def get_action(self): # return move

        if len(self.board.availables) == 1:
            return self.board.availables[0] # 棋盤只剩最后一個落子位置,直接返回

        # 每次計算下一步時都要清空plays和wins表,因為經過AI和玩家的2步棋之后,整個棋盤的局面發生了變化,原來的記錄已經不適用了——原先普通的一步現在可能是致勝的一步,如果不清空,會影響現在的結果,導致這一步可能沒那么“致勝”了
        self.plays = {} 
        self.wins = {}
        simulations = 0
        begin = time.time()
        while time.time() - begin < self.calculation_time:
            board_copy = copy.deepcopy(self.board)  # 模擬會修改board的參數,所以必須進行深拷貝,與原board進行隔離
            play_turn_copy = copy.deepcopy(self.play_turn) # 每次模擬都必須按照固定的順序進行,所以進行深拷貝防止順序被修改
            self.run_simulation(board_copy, play_turn_copy) # 進行MCTS
            simulations += 1

        print("total simulations=", simulations)

        move = self.select_one_move() # 選擇最佳着法
        location = self.board.move_to_location(move)
        print('Maximum depth searched:', self.max_depth)

        print("AI move: %d,%d\n" % (location[0], location[1]))

        return move

    def run_simulation(self, board, play_turn):
        """
        MCTS main process
        """

        plays = self.plays
        wins = self.wins
        availables = board.availables

        player = self.get_player(play_turn) # 獲取當前出手的玩家
        visited_states = set() # 記錄當前路徑上的全部着法
        winner = -1
        expand = True

        # Simulation
        for t in range(1, self.max_actions + 1):
            # Selection
            # 如果所有着法都有統計信息,則獲取UCB最大的着法
            if all(plays.get((player, move)) for move in availables):
                log_total = log(
                    sum(plays[(player, move)] for move in availables))
                value, move = max(
                    ((wins[(player, move)] / plays[(player, move)]) +
                     sqrt(self.confident * log_total / plays[(player, move)]), move)
                    for move in availables) 
            else:
                # 否則隨機選擇一個着法
                move = choice(availables)

            board.update(player, move)

            # Expand
            # 每次模擬最多擴展一次,每次擴展只增加一個着法
            if expand and (player, move) not in plays:
                expand = False
                plays[(player, move)] = 0
                wins[(player, move)] = 0
                if t > self.max_depth:
                    self.max_depth = t

            visited_states.add((player, move))

            is_full = not len(availables)
            win, winner = self.has_a_winner(board)
            if is_full or win: # 游戲結束,沒有落子位置或有玩家獲勝
                break

            player = self.get_player(play_turn)

        # Back-propagation
        for player, move in visited_states:
            if (player, move) not in plays:
                continue
            plays[(player, move)] += 1 # 當前路徑上所有着法的模擬次數加1
            if player == winner:
                wins[(player, move)] += 1 # 獲勝玩家的所有着法的勝利次數加1

    def get_player(self, players):
        p = players.pop(0)
        players.append(p)
        return p

    def select_one_move(self):
        percent_wins, move = max(
            (self.wins.get((self.player, move), 0) /
             self.plays.get((self.player, move), 1),
             move)
            for move in self.board.availables) # 選擇勝率最高的着法

        return move

    def has_a_winner(self, board):
        """
        檢查是否有玩家獲勝
        """
        moved = list(set(range(board.width * board.height)) - set(board.availables))
        if(len(moved) < self.n_in_row + 2):
            return False, -1

        width = board.width
        height = board.height
        states = board.states
        n = self.n_in_row
        for m in moved:
            h = m // width
            w = m % width
            player = states[m]

            if (w in range(width - n + 1) and
                len(set(states[i] for i in range(m, m + n))) == 1): # 橫向連成一線
                return True, player

            if (h in range(height - n + 1) and
                len(set(states[i] for i in range(m, m + n * width, width))) == 1): # 豎向連成一線
                return True, player

            if (w in range(width - n + 1) and h in range(height - n + 1) and
                len(set(states[i] for i in range(m, m + n * (width + 1), width + 1))) == 1): # 右斜向上連成一線
                return True, player

            if (w in range(n - 1, width) and h in range(height - n + 1) and
                len(set(states[i] for i in range(m, m + n * (width - 1), width - 1))) == 1): # 左斜向下連成一線
                return True, player

        return False, -1

    def __str__(self):
        return "AI"

Human 類

用於獲取玩家的輸入,作為落子位置。

class Human(object):
    """
    human player
    """

    def __init__(self, board, player):
        self.board = board
        self.player = player

    def get_action(self):
        try:
            location = [int(n, 10) for n in input("Your move: ").split(",")]
            move = self.board.location_to_move(location)
        except Exception as e:
            move = -1
        if move == -1 or move not in self.board.availables:
            print("invalid move")
            move = self.get_action()
        return move

    def __str__(self):
        return "Human"

Game 類

控制游戲的進行,並在終端顯示游戲的實時狀態。

class Game(object):
    """
    game server
    """

    def __init__(self, board, **kwargs):
        self.board = board
        self.player = [1, 2] # player1 and player2
        self.n_in_row = int(kwargs.get('n_in_row', 5))
        self.time = float(kwargs.get('time', 5))
        self.max_actions = int(kwargs.get('max_actions', 1000))

    def start(self):
        p1, p2 = self.init_player()
        self.board.init_board()

        ai = MCTS(self.board, [p1, p2], self.n_in_row, self.time, self.max_actions)
        human = Human(self.board, p2)
        players = {}
        players[p1] = ai
        players[p2] = human
        turn = [p1, p2]
        shuffle(turn) # 玩家和電腦的出手順序隨機
        while(1):
            p = turn.pop(0)
            turn.append(p)
            player_in_turn = players[p]
            move = player_in_turn.get_action()
            self.board.update(p, move)
            self.graphic(self.board, human, ai)
            end, winner = self.game_end(ai)
            if end:
                if winner != -1:
                    print("Game end. Winner is", players[winner])
                break

    def init_player(self):
        plist = list(range(len(self.player)))
        index1 = choice(plist)
        plist.remove(index1)
        index2 = choice(plist)

        return self.player[index1], self.player[index2]

    def game_end(self, ai):
        """
        檢查游戲是否結束
        """
        win, winner = ai.has_a_winner(self.board)
        if win:
            return True, winner
        elif not len(self.board.availables):
            print("Game end. Tie")
            return True, -1
        return False, -1

    def graphic(self, board, human, ai):
        """
        在終端繪制棋盤,顯示棋局的狀態
        """
        width = board.width
        height = board.height

        print("Human Player", human.player, "with X".rjust(3))
        print("AI    Player", ai.player, "with O".rjust(3))
        print()
        for x in range(width):
            print("{0:8}".format(x), end='')
        print('\r\n')
        for i in range(height - 1, -1, -1):
            print("{0:4d}".format(i), end='')
            for j in range(width):
                loc = i * width + j
                if board.states[loc] == human.player:
                    print('X'.center(8), end='')
                elif board.states[loc] == ai.player:
                    print('O'.center(8), end='')
                else:
                    print('_'.center(8), end='')
            print('\r\n\r\n')

增加簡單策略

實際運行時,當棋盤較小(6X6),需要連成一線的棋子數量較少(4)時,算法發揮的水平不錯,但是當棋盤達到8X8進行五子棋游戲時,即使將算法運行的時間調整到10秒,算法的發揮也不太好,雖然更長的時間效果會更好,但是游戲體驗就實在是差了。因此考慮增加一個簡單的策略:當不是所有着法都有統計信息時,不再進行隨機選擇,而是優先選擇那些在當前棋盤上已有落子的鄰近位置中沒有統計信息的位置進行落子,然后選擇那些離得遠的、沒有統計信息的位置進行落子,總得來說就是盡可能快速地讓所有着法具有統計信息。對於五子棋來說,關鍵的落子位置不會離現有棋子太遠。
下面是引入新策略的代碼:

def run_simulation(self, board, play_turn):

    for t in range(1, self.max_actions + 1):
        if ...
            ...
        else:
            adjacents = []
            if len(availables) > self.n_in_row:
                adjacents = self.adjacent_moves(board, player, plays) # 沒有統計信息的鄰近位置

            if len(adjacents):
                move = choice(adjacents)
            else:
                peripherals = []
                for move in availables:
                    if not plays.get((player, move)):
                        peripherals.append(move) # 沒有統計信息的外圍位置
                move = choice(peripherals) 
    ...

def adjacent_moves(self, board, player, plays):
    """
    獲取當前棋局中所有棋子的鄰近位置中沒有統計信息的位置
    """
    moved = list(set(range(board.width * board.height)) - set(board.availables))
    adjacents = set()
    width = board.width
    height = board.height

    for m in moved:
        h = m // width
        w = m % width
        if w < width - 1:
            adjacents.add(m + 1) # 右
        if w > 0:
            adjacents.add(m - 1) # 左
        if h < height - 1:
            adjacents.add(m + width) # 上
        if h > 0:
            adjacents.add(m - width) # 下
        if w < width - 1 and h < height - 1:
            adjacents.add(m + width + 1) # 右上
        if w > 0 and h < height - 1:
            adjacents.add(m + width - 1) # 左上
        if w < width - 1 and h > 0:
            adjacents.add(m - width + 1) # 右下
        if w > 0 and h > 0:
            adjacents.add(m - width - 1) # 左下

    adjacents = list(set(adjacents) - set(moved))
    for move in adjacents:
        if plays.get((player, move)):
            adjacents.remove(move)
    return adjacents

現在算法的效果就有所提升了。

下面是運行效果:

完整的代碼在Githubn_in_row_not_so_correct.py。


2017.2.23更新

UCT RAVE

論文《Monte-Carlo tree search and rapid action value estimation in computer Go》提到了UCT的一種改進方法,叫做UCT RAVE(Rapid action value estimate),提到RAVE,就不得不先提AMAF(All moves as first):它視使棋盤達到某一相同狀態的所有的着法都是等價的,不論是由誰在何時完成的,以下圖為例:

圖片引自論文《Monte Carlo Tree Search and Its Applications》,在經過A、B、C三步后得到當前的盤面狀態,這三步的順序是可能不相同的,但是得到的狀態是相同的,所以不作區分。
RAVE是基於AMAF的,它視一個着法在對當前盤面的所有模擬中是相同的,不論它出現在何種子狀態下、或是由哪個玩家給出的,如下圖:

圖片引自論文《Monte-Carlo tree search and rapid action value estimation in computer Go》。ab表示2中可能的着法,葉子節點中的數字表示是否獲勝。
對於狀態s來說,如果使用MC的統計方法,那么(a, s)出現的次數是2,勝利的次數是0,而(b, s)出現的次數是3,勝利的次數是2,根據勝率,應該選擇着法b;如果使用RAVE的統計方法,那么着法a在狀態s的所有子樹中出現的次數5(不論它在第幾層——即不論它的父節點是否是s,也不論是由哪個玩家進行的,只要是在狀態s所進行的模擬中——也就是s的子樹t(s)中即可),勝利的次數是3,着法b在狀態s的所有子樹中出現的次數5,勝利的次數是2,根據勝率,應該選擇着法a。可以看到,對於相同的情況,兩種方法可能給出不同的選擇。
根據下面的公式評估2種方法,作出最終的選擇,其中$Q(s, a) \(是MC的值,\)\widetilde{Q}(s, a)$是AMAF的值,這實際上就是MC RAVE

\[Q_{\star}(s, a) = (1-\beta (s, a))Q(s, a) + \beta (s, a)\widetilde{Q}(s, a) \]

下面是一種比較簡單的\(\beta\)計算方法:

\[\beta (s, a) = \sqrt{\frac{k}{3N(s)+k}} \]

N(s)是狀態s總共進行的模擬次數,當k=N(s)時,\(\beta\)等於1/2,即二者權重等價,不難看出,N(s)很大時,即模擬次數很多,\(\beta (s, a)\approx 0\),$Q(s, a) \(MC的值的權重大,`N(s)`很小時,即模擬次數較少的時候,\)\beta (s, a)\approx 1\(,\)\widetilde{Q}(s, a) $AMAF的值的權重大。
可以根據算法實際運行的情況來選擇k的值,論文給出的實驗結果表明k大於1000的時候效果較好。
在得到MC RAVE后,UCT RAVE就水到渠成了:

\[Q_{\star}^{\bigoplus }=Q_{\star}(s, a) + \sqrt{\frac{c*log(N(s))}{N(s, a)}} \]

對於之前算法實現的質疑

在了解了RAVE之后,再看之前實現的算法,實際上是有問題的,它很像RAVE,也很像MC,但實際都沒有實現正確:對於上面的t(s)樹,之前的算法認為棋盤上所有合法的位置都是s的子節點,以a為例(對於之前的算法來說,這里的a實際上指的是棋盤上的某個位置),對於玩家player1,它統計的是(player1, a),當player1在某次模擬(無論是否是在模擬的第一步就走出了着法a)走出了a,它統計了,對於另一個玩家player2走出的a,它沒有統計,如果以AMAF來考慮的話,a總的模擬次數應該是(player1, a)(player2, a)之和,但是對於勝利次數,(player1,a)統計的是player1在走出a並獲勝的次數,未統計player2在走出aplayer1獲勝的次數,和棋不進行統計,所以說之前的算法既不算是RAVE,也不是MC!
問題是它的表現還不錯,以至於我在繼續看論文之前沒有意識到它是有問題的,甚至在我意識到它有問題之前,還實現了一個基於它的UCT RAVE版本,而且表現進一步提升,下面是核心的代碼;

def run_simulation(self, board, play_turn):
    """
    MCTS main process
    """

    plays = self.plays # 統計RAVE中的MC部分的值
    wins = self.wins
    plays_rave = self.plays_rave # 統計RAVE中的AMAF部分的值
    wins_rave = self.wins_rave
    availables = board.availables

    player = self.get_player(play_turn)
    visited_states = set()
    winner = -1
    expand = True
    # Simulation
    for t in range(1, self.max_actions + 1):
        # Selection
        if all(plays.get((player, move)) for move in availables):
            value, move = max(
                ((1-sqrt(self.equivalence/(3 * plays_rave[move] + self.equivalence))) * (wins[(player, move)] / plays[(player, move)]) +
                    sqrt(self.equivalence/(3 * plays_rave[move] + self.equivalence)) * (wins_rave[move][player] / plays_rave[move]) + 
                    sqrt(self.confident * log(plays_rave[move]) / plays[(player, move)]), move)
                for move in availables)   # UCT RAVE  公式: (1-beta)*MC + beta*AMAF + UCB
        else:
            adjacents = []
            if len(availables) > self.n_in_row:
                adjacents = self.adjacent_moves(board, player, plays)

            if len(adjacents):
                move = choice(adjacents)
            else:
                peripherals = []
                for move in availables:
                    if not plays.get((player, move)):
                        peripherals.append(move)
                move = choice(peripherals)

        board.update(player, move)

        # Expand
        if expand and (player, move) not in plays:
            expand = False
            plays[(player, move)] = 0
            wins[(player, move)] = 0
            if move not in plays_rave:
                plays_rave[move] = 0 # 統計全部模擬中此着法的使用次數,不論是由誰在何時給出的
            if move in wins_rave:
                wins_rave[move][player] = 0 # 統計全部模擬中給出此着法的不同玩家的勝利次數
            else:
                wins_rave[move] = {player: 0}
            if t > self.max_depth:
                self.max_depth = t

        visited_states.add((player, move))

        is_full = not len(availables)
        win, winner = self.has_a_winner(board)
        if is_full or win:
            break

        player = self.get_player(play_turn)

    # Back-propagation
    for player, move in visited_states:
        if (player, move) in plays:
            plays[(player, move)] += 1 
            if player == winner:
                wins[(player, move)] += 1 
        if move in plays_rave:
            plays_rave[move] += 1 # 本次模擬中該着法只要被使用就增加1
            if winner in wins_rave[move]:
                wins_rave[move][winner] += 1 #本次模擬中使用該着法的並獲勝的玩家的勝利次數增加1

它在較短的時間內,如5s或10s內的表現是優於下面將要說明的實現的,因為短時間內它的模擬次數更多,這也符合蒙特卡洛方法的原理。完整的代碼在Github中的n_in_row_uct_rave_not_so_correct.py

正確的MC與UCT RAVE實現——也許?

下面是根據論文以及我的理解重新實現的UCT RAVE,因為水平有限,不一定對,把核心代碼放在這里,歡迎大家與我一起討論。

def run_simulation(self, board, play_turn):
    """
    UCT RAVE main process
    """

    plays = self.plays # 統計RAVE中的MC部分的值
    wins = self.wins
    plays_rave = self.plays_rave # 統計RAVE中的AMAF部分的值
    wins_rave = self.wins_rave
    availables = board.availables

    player = self.get_player(play_turn)
    winner = -1
    expand = True
    states_list = []
    # Simulation
    for t in range(1, self.max_actions + 1):
        # Selection
        state = board.current_state() # 棋盤的當前狀態
        actions = [(move, player) for move in availables]
        if all(plays.get((action, state)) for action in actions):
            total = 0
            for a, s in plays:
                if s == state:
                    total += plays.get((a, s)) # N(s)
            beta = self.equivalence/(3 * total + self.equivalence)

            value, action = max(
                ((1 - beta) * (wins[(action, state)] / plays[(action, state)]) +
                    beta * (wins_rave[(action[0], state)][player] / plays_rave[(action[0], state)]) + 
                    sqrt(self.confident * log(total) / plays[(action, state)]), action)
                for action in actions)   ## UCT RAVE  公式: (1-beta)*MC + beta*AMAF + UCB

        else:
            action = choice(actions)           
        
        move, p = action
        board.update(player, move)

        # Expand
        if expand and (action, state) not in plays:
            expand = False
            plays[(action, state)] = 0 # action是(move,player)。在棋盤狀態s下,玩家player給出着法move的模擬次數
            wins[(action, state)] = 0  # 在棋盤狀態s下,玩家player給出着法move並勝利的次數

            if t > self.max_depth:
                self.max_depth = t

        states_list.append((action, state)) # 當前模擬的路徑

        # 路徑上新增加的節點是前面所有節點的子節點,存在於前面各個節點的子樹中 
        for (m, pp), s in states_list:
            if (move, s) not in plays_rave:
                plays_rave[(move, s)] = 0  # 棋盤狀態s下的着法move的模擬次數,不論是由誰在何時給出的
                wins_rave[(move, s)] = {}   # 棋盤狀態s下着法move中記錄着所有玩家在該着法move出現的時候的勝利次數,不論是由誰在何時給出的
                for p in self.play_turn:
                    wins_rave[(move, s)][p] = 0

        is_full = not len(availables)
        win, winner = self.has_a_winner(board)
        if is_full or win:
            break

        player = self.get_player(play_turn)

    # Back-propagation
    for i, ((m_root, p), s_root) in enumerate(states_list):
        action = (m_root, p)
        if (action, s_root) in plays:
            plays[(action, s_root)] += 1 
            if player == winner and player in action:
                wins[(action, s_root)] += 1 

        for ((m_sub, p), s_sub) in states_list[i:]:
            plays_rave[(m_sub, s_root)] += 1 # 狀態s_root的所有子節點的模擬次數增加1
            if winner in wins_rave[(m_sub, s_root)]:                
                wins_rave[(m_sub, s_root)][winner] += 1 # 在狀態s_root的所有子節點中,將獲勝的玩家的勝利次數增加1

這個算法要得到一個較好的選擇需要更多的時間/更多的模擬次數,同時在實際使用的過程中,\(\beta\)的選擇也很關鍵的,如果使用前面介紹的公式,需要經過很多模擬比如10000次才能有一個較好的着法。其他的方法就比較復雜了,甚至可以結合機器學習。當總的模擬次數不多的時候,k的值不宜過大,我在模擬時間等於15s左右的時候,平均的模擬次數在3000次左右,這個時候取k=1000時算法表現較好。

之所以同樣的時間模擬次數比前面有問題的算法要少得多,是因為隨着模擬的進行,plays等中的內容會遠多於前面的算法,所以遍歷就需要更多的時間。實際上算法在做出了選擇之后,有一部分節點就不再需要了,這個時候就可以進行剪枝,下面是一種簡單直接的剪枝方法:

def prune(self):
        """
        根據狀態的長度進行剪枝
        """
        length = len(self.board.states) # 當前棋盤的狀態
        keys = list(self.plays)
        for a, s in keys:
            if len(s) < length + 2: # 現在算法已給出了選擇,但尚未更新到棋盤,在下一次模擬的時候,狀態s的長度比現在的增加了2(AI的選擇和玩家的選擇)
                del self.plays[(a, s)] # 所有狀態s的長度小於下一次模擬開始時狀態的長度的節點已經不再需要了
                del self.wins[(a, s)]

        keys = list(self.plays_rave)
        for m, s in keys:
            if len(s) < length + 2:
                del self.plays_rave[(m, s)]
                del self.wins_rave[(m, s)]

完整的代碼見Githubn_in_row_uct_rave.py
正是因為單位時間內模擬的次數較少,所以短時間內的表現上不如之前的算法,可以通過多線程等手段來進一步提高單位時間內的模擬次數,這也是下一講要進行的工作之一。

參考資料

[1] Jeff Bradberry “Introduction to Monte Carlo Tree Search”,Github.
[2] 徐心和, 徐長明. 計算機博弈原理與方法學概述[J]. 中國人工智能進展, 2009: 1-13.
[3] Gelly S, Silver D. Monte-Carlo tree search and rapid action value estimation in computer Go[J]. Artificial Intelligence, 2011, 175(11): 1856-1875.
[4] Magnuson M. Monte Carlo Tree Search and Its Applications[J]. Scholarly Horizons: University of Minnesota, Morris Undergraduate Journal, 2015, 2(2): 4.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM