Q Learning 迷宮走法

底下使用迷宮的走法,走到 (1,2) 或 (2, 1) 為地獄,得 -1分,走到 (2,2) 為天堂,得 1 分。本例使用 tkinter 繪製方格。

Q-Learning 應用在此例,計算每一格往[上,下,左,右] 四個方向的分數,公式與上一篇相同

$(Q(s,a)=Q(s,a)+lr[r+\gamma*maxQ(s’)-Q(s,a)])$

Q 表一樣是二維表格,但每列有四行資料

           上   下   左   右
(0, 0)    0.0  0.0  0.0  0.0
(0, 1)    0.0  0.0  0.0  0.0
(1, 1)    0.0  0.0  0.0  0.0
terminal  0.0  0.0  0.0  0.0

主程式

主程式如下

from Brain import Brain
from Maze import Maze
import pandas as pd

def update():
    for epoch in range(100):
        #初始化 state 觀測值
        s = maze.reset()
        while True:
            #更新 tkinter
            maze.render()
            # RL 依 state 觀測值選取動作
            action = rl.choose_action(str(s))
            # RL 執行動作後,取得一下個狀態觀測值、回報值,並檢查是否完成(掉入天堂或升上天堂)
            s_next, reward, done = maze.step(action)
            # RL 從 (狀態, 動作, 回報值) 學習
            rl.learn(str(s), action, reward, str(s_next))
            # 轉換到下一個狀態
            s = s_next
            # 進入地獄或天堂就中止
            if done:
                break
        print(f'epoch : {epoch}')
    print(f"=====================final table=====================")
    df=pd.DataFrame(
        {"up":rl.table[0],
         "down":rl.table[1],
         "left":rl.table[2],
         "right":rl.table[3],
         "max":rl.table.max(axis=1)
         },
        index=rl.table.index
    )
    print(df)
    maze.destroy()

if __name__ == "__main__":
    maze = Maze()
    rl = Brain(actions=list(range(4)))
    #rl=Brain()
    maze.after(100, update)
    maze.mainloop()

繪製迷宮

迷宮繪製程式如下,Maze.py

import random
import threading
import time
import tkinter as tk
import numpy as np
cols=4#4行
rows=4#4列
unit=80#每格40像素
gap=5

class Maze(tk.Tk):
    def __init__(self):
        super().__init__()
        #產生一個畫布
        self.canvas=tk.Canvas(
            self,
            bg='white',
            width=cols*unit,
            height=rows*unit
        )
        self.center=np.array([unit//2, unit//2])
        #垂直線
        for x in range(0, cols*unit, unit):
            self.canvas.create_line(x, 0, x, rows*unit)
        #水平線
        #水平線
        for y in range(0, rows*unit, unit):
            self.canvas.create_line(0, y, cols*unit, y)
        self.hell1=self.rectangle(2,1,"black")
        self.hell2 = self.rectangle(1, 2, "black")
        self.heaven=self.oval(2,2,"yellow")
        self.rect=self.rectangle(0,0,'red')
        self.canvas.pack()#最後一定要 pack,才會顯示圖形
        self.reset()
        # self.thread=threading.Thread(target=self.rnd)
        # self.thread.start()
    def reset(self):
        time.sleep(0.02)
        self.canvas.delete(self.rect)
        self.rect=self.rectangle(0,0,"red")
        self.update()
        return 0,0
    def render(self):
        time.sleep(0.01)
        self.update()
    def step(self, action):
        x1, y1, x2, y2=self.canvas.coords(self.rect)#取得矩型的左上及右下座標
        #mx : x軸的移動距離
        #my : y軸的移動距離
        mx, my = 0, 0
        if action==0:#往上
            if y1>unit:#s[0]紅色左上角的x值,s[1]紅色左上角的 y 值
                my -= unit
        elif action==1:#往下
            if y1<(rows-1)*unit:
                my += unit
        elif action==2:#往左
            if x1>unit:
                mx -= unit
        elif action==3:#往右
            if x1<(cols-1)*unit:
                mx += unit
        self.canvas.move(self.rect, mx, my)
        s_next=self.canvas.coords(self.rect)
        #計算回報值
        if s_next == self.canvas.coords(self.heaven):
            reward = 1#到天堂的回報值為 1
            done=True
            s_next='terminal'
        elif s_next in [self.canvas.coords(self.hell1), self.canvas.coords(self.hell2)]:
            reward = -1#到地獄的回報值為 -1
            done=True
            s_next = 'terminal'
        else:
            #s_next(row, col)
            s_next = (int(s_next[1]/unit), int(s_next[0]/unit))
            reward=0
            done=False
        time.sleep(0.01)
        return s_next, reward, done
    def coordinate(self, x, y):
        c=self.center+np.array([x*unit, y*unit])
        return c[0]-(unit//2-gap), c[1]-(unit//2-gap), c[0]+(unit//2-gap), c[1]+(unit//2-gap)
    def rectangle(self,x, y, color):
        x1, y1, x2, y2 = self.coordinate(x, y)
        return self.canvas.create_rectangle(x1, y1, x2, y2, fill=color)
    def oval(self, x, y, color):
        x1, y1, x2, y2 = self.coordinate(x, y)
        return self.canvas.create_oval(x1, y1, x2, y2, fill=color)
    #底下是動畫用的
    def rnd(self):
        while True:
            x=random.randint(0, cols-1)
            y=random.randint(0, rows-1)
            self.canvas.delete(self.rect)
            self.rect=self.rectangle(x, y, "red")
            self.update()
            time.sleep(0.02)

強化學習

強化學習 Brain.py 如下

import pandas as pd
import numpy as np

class Brain():
    def __init__(self, actions, learning_rate=0.01, reward_decay=0.9, e_greedy=0.9):
        #以下是在 main 中自已定的 0:up, 1:down, 2:left, 3:right
        self.actions=actions#[0,1,2,3]
        self.lr=learning_rate
        self.gamma=reward_decay
        self.epsilon=e_greedy
        self.table=pd.DataFrame(columns=self.actions, dtype=np.float64)

    def choose_action(self, s):
        self.check_state_exits(s)
        if np.random.uniform()<self.epsilon:#驗証
            state_action=self.table.loc[s,:]
            action=np.random.choice(state_action[state_action==np.max(state_action)].index)
        else:#隨機選取上下左右的動作
            action=np.random.choice(self.actions)
        return action
    def q_value(self, s, action, reward, s_next):
        self.check_state_exits(s_next)
        if s_next != 'terminal':
            target=reward + self.gamma*self.table.loc[s_next,:].max()
        else:
            target=reward
        self.table.loc[s,action]+=self.lr*(target-self.table.loc[s, action])
    def check_state_exits(self,s):
        if s not in self.table.index:
            s=pd.Series(
                [0]*len(self.actions),
                index=self.table.columns,
                name=s,
            )
            self.table=pd.concat([self.table, pd.DataFrame(s).T])#T置轉90度,也就是直向變橫向
            print("==============新狀態===============")
            print(self.table)

最後結果

在 final table 藍色的部份就是下一步要走的方向

epoch : 0
=====================新狀態=====================
          0    1    2    3
(0, 0)  0.0  0.0  0.0  0.0
=====================新狀態=====================
          0    1    2    3
(0, 0)  0.0  0.0  0.0  0.0
(1, 0)  0.0  0.0  0.0  0.0
=====================新狀態=====================
.......
epoch : 99
=====================final table=====================
                    up          down          left         right           max
(0, 0)    5.266589e-11  4.782969e-17  1.204786e-06  1.480376e-04  1.480376e-04
(0, 1)    1.169576e-05  1.801575e-08  0.000000e+00  1.194645e-03  1.194645e-03
(1, 1)    3.280616e-06 -1.990000e-02  0.000000e+00 -1.990000e-02  3.280616e-06
terminal  0.000000e+00  0.000000e+00  0.000000e+00  0.000000e+00  0.000000e+00
(1, 0)    0.000000e+00  0.000000e+00  0.000000e+00  1.779782e-09  1.779782e-09
(0, 2)    1.864988e-05 -5.851985e-02  6.778068e-07  8.050405e-03  8.050405e-03
(0, 3)    0.000000e+00  4.487453e-02  4.985308e-06  3.576159e-04  4.487453e-02
(2, 0)    0.000000e+00  0.000000e+00  0.000000e+00 -2.970100e-02  0.000000e+00
(1, 3)    3.222018e-04  1.898110e-01 -1.990000e-02  5.586076e-04  1.898110e-01
(2, 3)    2.864037e-03  0.000000e+00  5.657687e-01  2.668971e-03  5.657687e-01
(3, 0)    0.000000e+00  0.000000e+00  0.000000e+00  0.000000e+00  0.000000e+00
(3, 1)   -1.000000e-02  0.000000e+00  0.000000e+00  0.000000e+00  0.000000e+00
(3, 2)    1.000000e-02  0.000000e+00  0.000000e+00  0.000000e+00  1.000000e-02
(3, 3)    4.721940e-03  0.000000e+00  0.000000e+00  0.000000e+00  4.721940e-03

上述藍色的部份,表示如果一開始位於 (0,0) 的位置,基於選擇最大值的原則,最佳走法為 :
(0,0) -> 往右(0,1) -> 往右(0,2) -> 往右(0,3) -> 往下(1,3) -> 往下(2,3) -> 往左(2,3)

參考 : https://mofanpy.com/tutorials/machine-learning/reinforcement-learning/tabular-q1

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *