強化學習簡單入門——訓練井字棋AI

MDP馬爾科夫決策過程

這個說來抽象,其實蠻簡單,這裏直接摘抄一下WIKI的定義
馬爾可夫決策過程是一個4 元組SAPaRa(S,A,P_ {a},R_ {a}) ,且
S是一組有限的狀態,A是一組有限的動作(或者,AsA_ {s}如 是狀態可提供的有限操作集s,Pass= Prst+1=sst=sat=aP_ {a}(s,s')= \ Pr(s_ {t + 1} = s'\mid s_ {t} = s,a_ {t} = a)是狀態s會經過動作a轉變到狀態 s’ 的概率,RassR_ {a}(s,s')是從狀態轉換後獲得的獎勵(或預期的獎勵)
也就是狀態間可以通過特定的動作進行轉換,而且動作有可能對應着獎勵。
在這裏插入圖片描述
我們的強化學習將基於MDP的假設進行。

值迭代算法

基於MDP假設的強化學習中,我們會用一個狀態獎勵(V表)來記錄在某個狀態下執行某個動作的預期獎賞。如果MDP的環境已知,即我們知道了states的總數和每個state對應的可能的actions,就可以用值迭代算法通過N次迭代計算出每個狀態對應的狀態值函數V(x)。迭代公式是
V(x)=maxaAxXPxxa(Rxxa+γV(x)) V'(x) = max_{a \in A} \sum_{x' \in X} P_{x \rightarrow x'}^{a}(R_{x \rightarrow x'}^{a}+\gamma V(x'))
即我們每次迭代對所有狀態按照上式進行一次更新,並且在每個狀態的前瞻動作-狀態對中,貪心地選擇最好的reward值來更新當前的V值。最終我們會得到一個收斂的V函數,在決策時,只需要在每一步的x時,選擇讓下一步的V值V(x)V(x')最大的動作a(xx)a(x\rightarrow x')即可。

Q-learning

但是事實往往並不如所願,想要完全獲悉一個MDP過程的全部state信息和action信息在正常任務中一般是不可行的。事實上我們常常發現,一個MDP中的狀態數會隨着獨立的unit數目指數級別增長,即使是鬥地主這種只有50幾張牌,3個玩家的遊戲,所對應的全部狀態也幾乎無法計算。而狀態對應的動作就更爲具有挑戰性了,畢竟狀態數已經很多,如果每種狀態對應的動作很多,很多時候我們只有在獲知狀態後才能得知下一步的動作。
爲此我們有一種不需要得知MDP的環境信息也能進行學習的算法,Q-learning。因爲現在我們不能得到狀態數,也就無法建立V表,但我們可以建立一種基於狀態-動作對的估值函數Q(x,a),也就是Q表。爲了代替上面的算法,這種算法允許agent通過在環境中不斷採樣來更新Q值,並在多次更新求平均的過程中達到收斂。
Q(x,a)=(1α)Q(x,a)+α(R(x,a)+γQ(x,a)) Q(x,a) = (1-\alpha)Q(x,a)+\alpha (R(x',a') + \gamma Q(x',a'))
其中alpha是學習率,表示Q值的更新速度;gamma是獎賞discount。其中x’的選取是依賴當前學習到的策略Q的,有時我們也會爲了探索而附加一些噪音。這種學習方法允許在線學習,而且可以幫助我們避開很多無效的狀態。這種讓agent親自在環境中探索並學習的方式也更易於應用。

學習井字棋

當我們要把上面的算法應用到實際場景中也會面臨一些難題。用一個最簡單的井字棋對弈問題爲例,這個問題的狀態空間總數是39=196833^9=19683,似乎並不是太多。但是其中有着相當多的無效狀態,因爲我們的對弈是每個玩家輪流下子,而且一旦一方勝利遊戲就會結束,再並且每一步的action都是不同的,都需要獨立計算。這就讓值迭代算法變得不可行。
而如果我們要用Q-learning來學習,又有其他問題。這個問題中我們讓agent在環境中採樣,就只能讓兩個agent互相下棋,而agent每下一步所獲得的reward都是無法獲知的。我們需要自己設計其他的評估策略來從遊戲的勝負平中得到獎賞信息。

  1. 井字棋遊戲是雙方平等的遊戲。每次一方落子後換邊下棋,只需要把黑白子反轉,就能用完全相同的評估策略來評估局勢。評估局勢的過程和action的關係並不大,在當前的狀態x採取某狀態變換到下一個狀態x’,局勢的好壞只取決於x’.因此我們的學習目標是一個值函數V(x)。
  2. 獎賞的設計。我們在遊戲結束後會判一方勝利或兩方平局,很顯然勝利的一方的所有決策都對這個勝利有幫助,我們會爲所有操作獎賞1,並用Q-learning爲所有狀態提供衰減過後的獎賞。同理,失敗的一方會受到-1的懲罰獎勵。如果兩者平局,我們的經驗告訴我們,後手方是井字棋遊戲中明顯處於劣勢的一方,我們也會給先手方一個小的懲罰-0.1,而後手方則會得到小的獎勵0.1。

具體實現

想讓AI學會玩遊戲我們至少要先做好一個遊戲。首先設計井字棋遊戲的基本邏輯和V表,我們的棋盤會用一個長度9的字符串表示,對己方、對方和空格分別用O X _代指。

# 首先設計井字棋遊戲的基本邏輯和V表

values = {}
#我們的棋盤會用一個長度9的字符串表示,對己方、對方和空格分別用O X _代指

def exchange(board):
    '''
    輸入:board,string,棋盤字符串
    輸出:交換OX後的棋盤字符串
    '''
    board = board.replace('O','0')
    board = board.replace('X','x')
    board = board.replace('0','X')
    board = board.replace('x','O')
    return board

def EnumAllStates(size = 9):
    '''
    輸出:所有可能的state字符串
    用於初始化values表
    '''
    prefix = ["O","X","_"]
    if size==1:
        return prefix
    postfix = EnumAllStates(size-1)
    ret = []
    for pre in prefix:
        for post in postfix:
            ret.append(pre+post)
    return ret


states = EnumAllStates()
for state in states:
    values[state] = 0

另外,我們會枚舉所有狀態來初始化V表。
然後我們實現井字棋遊戲的邏輯判斷,包括基礎的勝負和平局。爲了讓agent能夠探索井字棋世界,我們會定義探索"任意落子後的下一個狀態"的函數,並定義一個策略,讓agent通過查V表來選擇最優的落子方案。

import numpy as np
import random

def WinCheck(board):
    '''
    輸入:board,string,棋盤字符串
    輸出:string,勝負判斷
    '''
    board = list(board)
    board = np.array(board).reshape(3,3)
    if (np.diag(board)=='O').all():
        return "win"
    elif (np.diag(np.flipud(board))=='O').all():
        return "win"
    elif (board[0]=='O').all() or (board[1]=='O').all() or (board[2]=='O').all():
        return "win"
    elif (board[:,0]=='O').all() or (board[:,1]=='O').all() or (board[:,2]=='O').all():
        return "win"
    elif (np.diag(board)=='X').all():
        return "lose"
    elif (np.diag(np.flipud(board))=='X').all():
        return "lose"
    elif (board[0]=='X').all() or (board[1]=='X').all() or (board[2]=='X').all():
        return "lose"
    elif (board[:,0]=='X').all() or (board[:,1]=='X').all() or (board[:,2]=='X').all():
        return "lose"
    elif '_' not in board.squeeze():
        return "draw"
    else:
        return "unknown"


def successors(board):
    '''
    得到board的所有落子選項對應的狀態
    並返回含有所有後續狀態的列表
    '''
    ret = []
    for i in range(9):
        if board[i]=='_':
            ret.append(board[:i]+'O'+board[i+1:])
    return ret

def policy(board, values, noise = 0):
    '''
    根據values做出決策,noise可以得到一些
    非最優選擇
    '''
    states = successors(board)
    if random.random()<noise:
        return random.choice(states)
    state = max(states,key = lambda key:values[key])
    return state

然後就是定義超參和訓練了,我這裏定義了10%的探索噪聲,0.8的折扣和0.2的學習率,爲勝利提供1的獎賞(失敗-1),爲後手平局提供0.1的獎賞(先手平局-0.1).

def train(values, lr = 0.2, noise = 0.1, iters = 100000,
         discount = 0.8, win_reward = 1, draw_reward = 0.1):
    '''
    通過對抗方式訓練values表
    '''
    Awin = 0
    Bwin = 0
    draw = 0
    
    
    for t in range(iters):
        pathA = []  # 先手的狀態路徑
        pathB = []  # 後手的狀態路徑
        player = -1
        path_dict = {-1:pathA,1:pathB}
        board = "_________"
        winner = 0  #0表示平局,1爲B獲勝,-1爲A獲勝
        while(1):
            # 得到下一步的落子策略
            state = policy(board, values, noise = noise)
            path_dict[player].append(state)
            # 勝負檢測
            info = WinCheck(state)
            if info=="win":
                winner = player
            elif info=="lose":
                winner = -player
            elif info=="draw":
                winner = 0
            if info!="unknown":
                break
            board = exchange(state) #交換O與X
            player = -player  #交換玩家
        # 更新values
        if winner==-1:
        # 獎勵A,懲罰B
            reward = win_reward
            Awin += 1
        elif winner==1:
            # 獎勵B,懲罰A
            reward = -win_reward
            Bwin += 1
        elif winner==0:
            # 輕微懲罰A,輕微獎勵B
            reward = -draw_reward
            draw += 1
        values[pathA[-1]] = (1-lr)*values[pathA[-1]]+lr*reward
        for i in range(len(pathA)-2,-1,-1):
            values[pathA[i]] = (1-lr)*values[pathA[i]]+\
            lr*(reward+values[pathA[i+1]]*discount)
        values[pathB[-1]] = (1-lr)*values[pathB[-1]]+lr*(-reward)
        for i in range(len(pathB)-2,-1,-1):
            values[pathB[i]] = (1-lr)*values[pathB[i]]+\
            lr*((-reward)+values[pathB[i+1]]*discount)
        if (t+1)%500==0:
            print("Iteration: %d"%(t+1))
            print("A win times: %d"%(Awin))
            print("B win times: %d"%(Bwin))
            print("Draw times: %d"%(draw))
            print("**---------------------**")
        
    return values

訓練十萬次,觀察勝利、失敗和平局次數變化。可以觀察到從A勝B的多一些,到兩者經常平局。我們認爲算法已經收斂。

# 我們可以用學習完畢的values來建立一個下棋bot
# 讓它和人類對弈來檢驗算法正確性

def PrintBoard(board):
    for i in range(3):
        for j in range(3):
            print(board[i*3+j],end = ' ')
        print()


def agent(values):
    player = input("您想要先手還是後手?(enter o or d)")
    player = -1 if player=='o' else 1
    board = "_________"
    PrintBoard(board)
    while(1):
        if player == -1:
            #人類下棋
            n = input("請輸入落子位置(1~9)")
            n = int(n)-1
            board = board[:n]+'O'+board[n+1:]
            PrintBoard(board)
            
        else:
            # 機器下棋
            print("Agent正在思考...")
            board = policy(board, values)
            board = exchange(board)
            PrintBoard(board)
            board = exchange(board)
        
        # 勝負檢測
        info = WinCheck(board)
        if info!="unknown":
            break
        
        player = -player
        board = exchange(board)
        
    print("Game over!")

agent(values)

我們還可以讓機器和人對弈;可以看見,想贏下現在的agent已經並不簡單了。

您想要先手還是後手?(enter o or d)o
_ _ _ 
_ _ _ 
_ _ _ 
請輸入落子位置(1~9)5
_ _ _ 
_ O _ 
_ _ _ 
Agent正在思考...
_ _ X 
_ O _ 
_ _ _ 
請輸入落子位置(1~9)6
_ _ X 
_ O O 
_ _ _ 
Agent正在思考...
_ _ X 
X O O 
_ _ _ 
請輸入落子位置(1~9)9
_ _ X 
X O O 
_ _ O 
Agent正在思考...
X _ X 
X O O 
_ _ O 
請輸入落子位置(1~9)8
X _ X 
X O O 
_ O O 
Agent正在思考...
X X X 
X O O 
_ O O 
Game over!


您想要先手還是後手?(enter o or d)d
_ _ _ 
_ _ _ 
_ _ _ 
Agent正在思考...
_ _ _ 
X _ _ 
_ _ _ 
請輸入落子位置(1~9)5
_ _ _ 
X O _ 
_ _ _ 
Agent正在思考...
_ _ X 
X O _ 
_ _ _ 
請輸入落子位置(1~9)1
O _ X 
X O _ 
_ _ _ 
Agent正在思考...
O _ X 
X O _ 
_ _ X 
請輸入落子位置(1~9)6
O _ X 
X O O 
_ _ X 
Agent正在思考...
O _ X 
X O O 
_ X X 
請輸入落子位置(1~9)7
O _ X 
X O O 
O X X 
Agent正在思考...
O X X 
X O O 
O X X 
Game over!

總結

這個強化學習的例子雖然簡單,我這裏實現只用了一個半小時,但是它包含了基礎的強化學習思想和算法。希望你能夠通過這個簡單的例子,學會使用Q-learning來解決一些簡單的問題。
當然如果你有興趣也可以深入學習,強化學習還有很多更先進的知識。像是用神經網絡代替Q表的DQL,啓發式的方法,值函數近似和梯度方法等。
以DQL爲例,如果我們要實現一個棋盤爲11x11的五子棋AI,則狀態空間到達31213^121那麼大,用Q表進行Q-learning是不現實的。爲此我們可以設計一個輸入是1x11x11的卷積神經網絡,並用這個神經網絡學習一個Q值的迴歸任務。我們知道卷積神經網絡有着很強的特徵提取能力,只要設計合適的卷積核大小(比如對五子棋的五子連珠使用5x5的卷積核,或兩層3x3的卷積核提取特徵),就能從當前的棋盤中提取到合適的pattern,並把這些pattern通過全連接層擬合成能表徵Q值的連續函數。神經網絡迴歸比起用Q表死記硬背要來得更爲高效和節約。前幾年大熱的AlphaGo就是用類似的技術實現的。當然,這個技術還可以用於很多其他領域。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章