我挑選的demo是書《深入淺出強化學習原理入門》裏的一道題,但是沒有答案,所以我想自己嘗試做一下。
(P.S.我真的對這本書很無感,後來發現豆瓣上基本全是對這本書的吐槽。反正,我一開始看得雲裏霧裏的,全書的邏輯性不強,總之不建議讀。想入門的萌新可以看李宏毅老師的RL課程,老師人很可愛,講得非常通俗易懂,不會讓你特別快地放棄,非常適合入門。唯一的不足就是老師講課中英文夾雜再加上臺灣腔,有時要聽幾遍才懂某些字眼是什麼意思,而且偶爾麥克風會發瘋,不過瑕不掩瑜,這還是一門好課。)
這道題是一個如下圖所示的迷宮,黃色的格子表示出口,另外每一個格子都用數字表示,它是agent的一個狀態,每個狀態都有上下左右四個動作,只是某些格子的某些動作是無用的,因爲它在邊界上,所以這時會保持現有狀態。
第一步,創建一個agent
-
創建class並完成初始化
需要事先安裝好gym這個模塊
我建立了一個class,是從gym.Env繼承過來的,方便我們在後續調用viewer的一些方法。在初始化函數裏放入狀態和動作的信息
class grid_env(gym.Env):
def __init__(self):
self.viewer=None
self.states=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18]
self.terminate_state=18
self.gamma = 0.9
self.state=self.states[int(np.random.random()*len(self.states))]
self.actions=['l','r','u','d']
t是一個字典,記錄了所有狀態-動作對應的下一個狀態,我這裏只展示了一小部分
#狀態動作轉移矩陣,沒有賦值的狀態動作,說明狀態沒有發生變化。
#在這個遊戲中,在某一狀態執行某一動作得到的下一個狀態是固定的,不具有隨機性
self.t=dict();
self.t['1_r']=2
self.t['1_d']=5
self.t['2_l']=1
self.t['2_r']=3
r同樣也是一個字典,記錄了部分狀態-動作對應的立即獎勵
#設置狀態-動作的立即獎勵,我挑選了離出口較近的幾個點,當這幾個點向出口走近時,立即獎勵爲1,到達出口的獎勵爲10
#遠離出口的獎勵爲-1
self.r=dict();
self.r['9_r']=1.0
self.r['10_r']=10.0
self.r['4_d']=1.0
self.r['8_d']=10.0
self.r['15_u']=10.0
self.r['9_u']=-1.0
self.r['9_d']=-1.0
self.r['10_l']=-1.0
self.r['10_d']=-1.0
self.r['8_u']=-1.0
self.r['15_l']=-1.0
cord這個字典則記錄了agent分別在18個狀態下的座標,座標是相應格子的中心位置,這裏只展示了三個狀態。
self.cord=dict();
self.cord[1]=[140,460]
self.cord[2]=[220,460]
self.cord[3]=[300,460]
#用pandas的DataFrame來做q_table的數據結構非常合適
self.q_table = pd.DataFrame(columns=self.actions, dtype=np.float64)
for s in self.states:
self.q_table = self.q_table.append(
pd.Series(
[0]*len(self.actions),
index=self.q_table.columns,
name=s,
)
)
還需要編寫一下reset函數,讓遊戲每次開始時隨機出現在某個位置,除了np.random.random,還可以用np.random.choice來隨機選擇狀態
def reset(self):
self.state=self.states[int(np.random.random()*len(self.states))]
return self.state
-
初始化之後,開始編寫重要的圖像引擎函數——render()
窗口的大小是600*600,我設置的迷宮是400*400的(並顯示在窗口正中間),每個格子是80*80的。我分別在水平方向和垂直方向畫了六條線,形成了一個5*5的格圖。然後用rendering.FilledPolygon()渲染出長方體形狀的障礙。最後繪製出人(這裏我用一個黃色的小圓表示,出口則是一個黑色的大圓),注意人的位置是由self.state來決定的,它是實時移動的。
注意,用rendering函數繪製完圖形後,一定要用viewer.add_geom(),將其加入到viewer中,最後才能顯示出來。
一開始我沒有設置close這個參數,在我不斷調用render()時,人以前的位置上也會出現黃色小圓,也就是它不會清除歷史信息。所以我就每次調用render()之前,先render(True),將之前的viewer關閉。但是這樣會有一個小問題:窗口不斷閃現,就是看着有點難受。我對圖像渲染這方面一無所知,google也沒找到解決方法,所以這個先擱置一下。
def render(self,close=False):
#這個close非常重要,沒有它的話,人在以前時刻的軌跡也會被保留
if close:
if self.viewer is not None:
self.viewer.close()
self.viewer = None
return
if self.viewer is None:
self.viewer = rendering.Viewer(600, 600)
for i in range(6):
line=rendering.Line((100+80*i,100),(100+80*i,500))
line.set_color(0,0,0)
self.viewer.add_geom(line)
for i in range(6):
line=rendering.Line((100,100+80*i),(500,100+80*i))
line.set_color(0,0,0)
self.viewer.add_geom(line)
l=340
r=420
b=340
t=500
cart= rendering.FilledPolygon([(l,b), (l,t), (r,t), (r,b)])
self.viewer.add_geom(cart)
l=100
r=260
b=260
t=340
cart= rendering.FilledPolygon([(l,b), (l,t), (r,t), (r,b)])
self.viewer.add_geom(cart)
l=260
r=500
b=100
t=180
cart= rendering.FilledPolygon([(l,b), (l,t), (r,t), (r,b)])
self.viewer.add_geom(cart)
#繪製出口
circle=rendering.make_circle(40)
trans=rendering.Transform(translation=(460,300))
circle.add_attr(trans)
self.viewer.add_geom(circle)
#繪製人所在的位置
[x,y]=self.cord[self.state]
circle=rendering.make_circle(30)
trans=rendering.Transform(translation=(x,y))
circle.add_attr(trans)
circle.set_color(1,0.9,0)
self.viewer.add_geom(circle)
return self.viewer.render()
-
編寫step函數
它相當於物理引擎,也就是模擬了環境,將選擇的動作與之互動,就返回給agent下一狀態,立即獎勵和是否終止的信號。在init時,我們只設置了部分狀態-動作的獎勵,其他的我沒規定。最初我把這些沒規定的狀態-動作對的獎勵都設置爲0.但是在測試時,我發現這就可能發生問題,人一直碰壁,而不產生任何有效動作。所以我把這些使人徘徊在原地的狀態-動作對的獎勵設置爲-0.5.從另一個角度想,這個負獎勵的加入在一定程度上可以促使人摒棄無效動作,在更短步數內走出迷宮。
def _step(self,action) :
state=self.state
if state==self.terminate_state:
return state,0,True
key="%d_%s"%(state,action)
is_terminal=False
#根據狀態-動作轉移矩陣來更新狀態
if key in self.t:
next_state=self.t[key]
else:
next_state=state
self.state=next_state
if next_state==self.terminate_state:
is_terminal=True
#判斷這一狀態動作
if key in self.r:
r=self.r[key]
elif key not in self.t:
r=-0.5
else:
r=0.0
return next_state,r,is_terminal
以上這些代碼就基本上把迷宮這個ENV的class構建好了。主要是step和render兩個方法,前者是環境的物理引擎,後者是用來渲染的圖像引擎(通常藉助於gym的viewer來編寫)
第二步 構建agent
在這個例子中,我用的方法是基於蒙特卡洛的q learning方法。所以需要一個函數來隨機採樣許多回合的數據。
#通過蒙特卡羅收集大量數據
def random_sample(self,num):
state_sample=[]
action_sample=[]
reward_sample=[]
for i in range(num):
tic=time.clock()
s_tmp=[]
r_tmp=[]
a_tmp=[]
s=self.reset()
is_stop=False
while is_stop==False:
s_tmp.append(s)
a=self.actions[int(np.random.random()*len(self.actions))]
s,r,is_stop=self._step(a)
r_tmp.append(r)
a_tmp.append(a)
state_sample.append(s_tmp)
action_sample.append(a_tmp)
reward_sample.append(r_tmp)
toc=time.clock()
print("epoch%d---sampling time is %fs"%(i,(toc-tic)))
return state_sample,action_sample,reward_sample
然後我們對這些數據進行統計分析。首先要把立即獎勵轉化成累積獎勵。也就是在某一狀態採取某一動作之後,一直到回合結束(走出迷宮),得到的獎勵總和。所以要將每一條回合的數據倒過來處理,注意要從倒數第二條開始處理,最後一條是結束時的狀態。
def mc(self,state_sample,action_sample,reward_sample):
nums=dict()
for a in self.actions:
nums[a]=0.0
nums_table=[]
for i in range(len(self.states)):
nums_table.append(nums)
for i in range(len(state_sample)):
G=0.0
tic=time.clock()
for j in range(len(state_sample[i])-1,-1,-1):
G*=self.gamma
G+=reward_sample[i][j]
a=action_sample[i][j]
s=state_sample[i][j]
self.q_table.loc[s,a]+=G
nums_table[s-1][a]+=1
toc=time.clock()
if i%100==0:
print("epoch%d---training time is %fs"%(i,(toc-tic)))
for s in self.states:
for a in self.actions:
self.q_table.loc[s,a]/=nums_table[s-1][a]
return self.q_table
當我們隨機採樣1000個回合的數據進行mc統計後,得到的q_table如下:
|
l |
r |
u |
d |
1 |
-0.21225 |
-0.12657 |
-0.19627 |
-0.15027 |
2 |
-0.14656 |
-0.12296 |
-0.17208 |
-0.11699 |
3 |
-0.11868 |
-0.16466 |
-0.16666 |
-0.08986 |
4 |
0.025052 |
0.021552 |
0.022692 |
0.056406 |
5 |
-0.18392 |
-0.11075 |
-0.15179 |
-0.18497 |
6 |
-0.13336 |
-0.076 |
-0.12637 |
-0.15179 |
7 |
-0.09813 |
-0.11197 |
-0.11517 |
-0.00195 |
8 |
0.026403 |
0.028949 |
0.01265 |
0.070143 |
9 |
-0.01863 |
0.19151 |
-0.10776 |
-0.04913 |
10 |
-0.03226 |
0.281304 |
0.061431 |
0.031505 |
11 |
-0.11952 |
-0.06204 |
-0.1215 |
-0.10037 |
12 |
-0.08944 |
0.015317 |
-0.08887 |
-0.09643 |
13 |
-0.04697 |
0.080801 |
0.013004 |
-0.0175 |
14 |
0.001236 |
0.149338 |
0.085545 |
0.049916 |
15 |
0.020852 |
0.075249 |
0.198882 |
0.055839 |
16 |
-0.14478 |
-0.10038 |
-0.09159 |
-0.1403 |
17 |
-0.1075 |
-0.13904 |
-0.06664 |
-0.13785 |
18 |
0 |
0 |
0 |
0 |
根據這個表格,每個狀態-動作對都會有一個最優的動作
狀態 |
動作 |
狀態 |
動作 |
1 |
右(下) |
10 |
右 |
2 |
下(右) |
11 |
右 |
3 |
下 |
12 |
右 |
4 |
下 |
13 |
右 |
5 |
右 |
14 |
右(上) |
6 |
右 |
15 |
上 |
7 |
下 |
16 |
上(右) |
8 |
下 |
17 |
上 |
9 |
右 |
18 |
結束 |
在1,2,14,16的狀態下,最優動作不是唯一,但在我訓練的q table中,它有一個自己的偏好,括號裏是另一個最優動作。對比q table,可以看到,如果某一狀態只存在唯一一個最優動作時,相應的q值要比其他動作的大的多一點;反之,則另外最優動作的q值和最大值的差別會比較小。
通過這個實驗我意識到獎勵的設置與結果的好壞有很大關係。