強化學習實戰:自定義Gym環境之井字棋


在文章 強化學習實戰 | 自定義Gym環境 中 ,我們了解了一個簡單的環境應該如何定義,並使用 print 簡單地呈現了環境。在本文中,我們將學習自定義一個稍微復雜一點的環境——井字棋。回想一下井字棋游戲:

  • 這是一個雙人回合制博弈游戲,雙方玩家使用的占位符是不一樣的(圈/叉),動作編寫需要區分玩家
  • 雙方玩家獲得的終局獎勵是不一樣的,勝方+1,敗方-1(除非平局+0),獎勵編寫需要區分玩家
  • 終局的條件是:任意行 / 列 / 對角 占滿了相同的占位符 or 場上沒有空位可以占位
  • 從單個agent的視角看,當前狀態 s 下采取動作 a 后,新的狀態 s_ 並不是后繼狀態,而是一個等待對手動作的中間狀態,真正的后繼狀態是對手動作之后產生的狀態 s'(除非采取動作 a 后游戲直接結束)。正如Sliver在課上回答同學關於多智能體應該如何處理時說的:“把其他agent也當作環境的一部分”,如下圖所示:
  •  

除了游戲本身的機制,考慮到與gym的API接口格式的契合,通過外部循環控制游戲進程是較方便的,所以env本身定義時不必要編寫控制游戲進程 / 切換行動玩家的代碼。另外,我們還需要更生動的環境呈現方式,而不是print!那么,接下來我們就來實現上述的目標吧!

步驟1:新建文件

來到目錄:D:\Anaconda\envs\pytorch1.1\Lib\site-packages\gym\envs\user,創建文件 __init__.py 和 TicTacToe_env.py(還記得嗎?文件夾user是文章 強化學習實戰 | 自定義Gym環境 中我們創建的用來存放自定義環境的文件夾)。

步驟2:編寫 TicTacToe_env.py 和 __init__.py

gym內置了一個繪圖工具rendering,不過功能並不周全,想要繪制復雜的東西非常麻煩。本文不打算深入研究,只借助rendering中基本的線條 / 方塊 / 圓圈呈現環境(更生動的游戲表現我們完全可以通過pygame來實現)。rendering是單幀繪制的,當調用env.render()時,將呈現當前 self.viewer.geoms 中所記錄的繪畫元素。環境的基本要素設計如下:

  • 狀態:由二維的numpy.array表示,無占位符值為0,有藍色占位符值為1,有紅色占位符值為-1。
  • 動作:設計為一個字典,有着格式:action = {'mark':'blue', 'pos':(x, y)},其中'mark'表示占位符的顏色,用以區分玩家,'pos'表示占位符的位置。
  • 獎勵:鎖定藍方視角,勝利+1,失敗-1,平局+0。

TicTacToe_env.py 的整體代碼如下:

import gym
import random
import time
import numpy as np
from gym.envs.classic_control import rendering

class TicTacToeEnv(gym.Env):
    def __init__(self):
        self.state = np.zeros([3, 3])
        self.winner = None
        WIDTH, HEIGHT = 300, 300 
        self.viewer = rendering.Viewer(WIDTH, HEIGHT)
    
    def reset(self):
        self.state = np.zeros([3, 3])
        self.winner = None
        self.viewer.geoms.clear() # 清空畫板中需要繪制的元素
        self.viewer.onetime_geoms.clear()
    
    def step(self, action):
        # 動作的格式:action = {'mark':'circle'/'cross', 'pos':(x,y)}# 產生狀態
        x = action['pos'][0]
        y = action['pos'][1]
        if action['mark'] == 'blue':  
            self.state[x][y] = 1
        elif action['mark'] == 'red': 
            self.state[x][y] = -1
        # 獎勵
        done = self.judgeEnd() 
        if done:
            if self.winner == 'blue': 
                reward = 1 
            else:
                reward = -1
        else: reward = 0
        # 報告
        info = {}
        return self.state, reward, done, info
      
    def judgeEnd(self):
        # 檢查兩對角
        check_diag_1 = self.state[0][0] + self.state[1][1] + self.state[2][2]
        check_diag_2 = self.state[2][0] + self.state[1][1] + self.state[0][2]
        if check_diag_1 == 3 or check_diag_2 == 3:
            self.winner = 'blue'
            return True
        elif check_diag_1 == -3 or check_diag_2 == -3:
            self.winner = 'red'
            return True
        # 檢查三行三列
        state_T = self.state.T
        for i in range(3):
            check_row = sum(self.state[i]) # 檢查行
            check_col = sum(state_T[i]) # 檢查列
            if check_row == 3 or check_col == 3:
                self.winner = 'blue'
                return True
            elif check_row == -3 or check_col == -3:
                self.winner = 'red'
                return True
        # 檢查整個棋盤是否還有空位
        empty = []
        for i in range(3):
            for j in range(3):
                if self.state[i][j] == 0: empty.append((i,j))
        if empty == []: return True
        
        return False
    
    def render(self, mode='human'):
        SIZE = 100
        # 畫分隔線
        line1 = rendering.Line((0, 100), (300, 100))
        line2 = rendering.Line((0, 200), (300, 200))
        line3 = rendering.Line((100, 0), (100, 300))
        line4 = rendering.Line((200, 0), (200, 300))
        line1.set_color(0, 0, 0)
        line2.set_color(0, 0, 0)
        line3.set_color(0, 0, 0)
        line4.set_color(0, 0, 0)
        # 將繪畫元素添加至畫板中
        self.viewer.add_geom(line1)
        self.viewer.add_geom(line2)
        self.viewer.add_geom(line3)
        self.viewer.add_geom(line4)
        # 根據self.state畫占位符
        for i in range(3):
            for j in range(3):
                if self.state[i][j] == 1:
                    circle = rendering.make_circle(30) # 畫直徑為30的圓
                    circle.set_color(135/255, 206/255, 250/255) # mark = blue
                    move = rendering.Transform(translation=(i * SIZE + 50, j * SIZE + 50)) # 創建平移操作
                    circle.add_attr(move) # 將平移操作添加至圓的屬性中
                    self.viewer.add_geom(circle) # 將圓添加至畫板中
                if self.state[i][j] == -1:
                    circle = rendering.make_circle(30)
                    circle.set_color(255/255, 182/255, 193/255) # mark = red
                    move = rendering.Transform(translation=(i * SIZE + 50, j * SIZE + 50))
                    circle.add_attr(move)
                    self.viewer.add_geom(circle)
            
        return self.viewer.render(return_rgb_array=mode == 'rgb_array')

在 __init__.py 中引入類的信息,添加:

from gym.envs.user.TicTacToe_env import TicTacToeEnv

步驟3:注冊環境

來到目錄:D:\Anaconda\envs\pytorch1.1\Lib\site-packages\gym,打開 __init__.py,添加代碼:

register(
    id="TicTacToeEnv-v0",
    entry_point="gym.envs.user:TicTacToeEnv",
    max_episode_steps=20,    
)

步驟4:測試環境

在測試代碼中,我們在主循環中讓游戲不斷地進行。藍紅雙方玩家以0.5s的間隔,隨機選擇空格子動作,代碼如下:

import gym
import random
import time

# 查看所有已注冊的環境
# from gym import envs
# print(envs.registry.all()) 

def randomAction(env_, mark): # 隨機選擇未占位的格子動作
    action_space = []
    for i, row in enumerate(env_.state):
        for j, one in enumerate(row):
            if one == 0: action_space.append((i,j))  
    action_pos = random.choice(action_space)
    action = {'mark':mark, 'pos':action_pos}
    return action

def randomFirst():
    if random.random() > 0.5: # 隨機先后手
        first_, second_ = 'blue', 'red'
    else: 
        first_, second_ = 'red', 'blue'
    return first_, second_

env = gym.make('TicTacToeEnv-v0')
env.reset() # 在第一次step前要先重置環境 不然會報錯
first, second = randomFirst()
while True:
    # 先手行動
    action = randomAction(env, first)
    state, reward, done, info = env.step(action)
    env.render()
    time.sleep(0.5)
    if done: 
        env.reset()
        env.render()
        first, second = randomFirst()
        time.sleep(0.5)
        continue
    # 后手行動
    action = randomAction(env, second)
    state, reward, done, info = env.step(action)
    env.render()
    time.sleep(0.5)
    if done: 
        env.reset()
        env.render()
        first, second = randomFirst()
        time.sleep(0.5)
        continue

效果如下圖所示:

 

后文:強化學習實戰 | 表格型Q-Learning玩井字棋(一)搭個框架

 

 


免責聲明!

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



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