之前看到一个强化学习有意思的小项目,在一维世界里寻找宝藏的一个小项目。具体可点击: 莫烦Python-强化学习小例子
对于莫先生的讲解我有一些自己的理解,注释在了以下代码中,以防遗忘。我根据自己的理解和想法将这个一维的世界扩展成了二维的,把这个项目扩展成了一个在二维的平面世界中寻找出口的一个小项目,当然核心算法仍然是Q-learning,不过项目变得更具扩展性了,如果日后有空闲,我打算再加上墙壁,将它变为一个迷宫寻路的小项目。
import numpy as np
import pandas as pd
import time
np.random.seed(2)
N_STATES = 6#1维世界的宽度,6个格子[0][1][2][3][4][5]
ACTIONS = ['left', 'right']#探索者的可用动作
EPSILON = 0.90#贪婪度greedy,90%的概率采取当前最优选择
ALPHA = 0.1#学习率
GAMMA = 0.9#可视奖励递减值
MAX_EPISODES = 20#最大回合数
FRESH_TIME = 0.1#移动间隔时间
def build_q_table(n_states, actions):#建立一个价值表,Q表
table = pd.DataFrame(np.zeros((n_states-1, len(actions))),columns=actions,)#n_states-1终点位置不需要Q值
#DataFrame建表函数,建立一个值全为0的表,其中列为动作,行为每一state
return table
def choose_action(state, q_table):#选择动作
state_actions = q_table.iloc[state, :]
#[state, :]表示以state表示的数字的那一行的所有数据(':'在这里是所有的意思,比如'2:'表示索引2(第三个)之后的所有元素)
if (np.random.uniform() > EPSILON) or (max(state_actions) == 0):#uniform(x,y,z)从均匀分布[x,y)中随机采样z个数据,默认值x=0,y=1,z=1;max最大值是0,则全为0
#从[0,1)中随机产生一个数,如果这个数大于EPSILON(0.9),或者如果Q表中state行两个数都等于0,那么执行下一步
action_name = np.random.choice(ACTIONS)#从['left', 'right']中随机选择一个动作执行
else:
action_name = state_actions.idxmax()#从Q表的state行里选择出最大值,得到它的索引left或者right
return action_name
def get_env_feedback(S, A):#环境反馈
if A == 'right':#如果动作是向右
if S == N_STATES - 2:#6-2=4
#总共6个格子[0][1][2][3][4][5],站在[4]上,下一个动作已经决定向右,则肯定到达终点
S_ = 'end'#下一个动作是终点,结束
Reward = 1#奖励为1
else:
S_ = S + 1#站在下一个格子上
Reward = 0#奖励为0
else:#如果动作是向左
Reward = 0#奖励为0,走左边永远到不了终点,奖励必须为0
if S == 0:#如果站在[0]上,已经选择向左
S_ = S#那么下一位置不变,仍站在[0]
else:
S_ = S - 1#其他位置上则向左挪一格
return S_, Reward
def update_env(S, episode, step_counter):#环境更新
env_list = ['-']*(N_STATES-1) + ['T']#一维世界是由[0][1][2][3][4]的'-'和一个'T'组成
if S == 'end':#如果站在终点上
a = 'Episode %s: total_steps = %s' % (episode+1, step_counter)
print('\r{}'.format(a), end='')#将光标移到本行的开头,重新打印输出,end=''不换行
time.sleep(1)#等待两秒
print('\r ', end='')#用空白刷新
else:#如果没在终点
env_list[S] = 'o'#探索者站在哪个格子上,哪个格子变成'o'
a = ''.join(env_list)
print('\r{}'.format(a), end='')#打印一维世界
time.sleep(FRESH_TIME)#等待移动间隔时间
def RL():#强化学习
q_table = build_q_table(N_STATES, ACTIONS)#实例化Q表
for episode in range(MAX_EPISODES):#在总回合数以内,每一回合
step_counter = 0#总步数计数器
S = 0#初始时站在[0]上
is_terminated = False#是否结束
update_env(S, episode, step_counter)#初始环境
while not is_terminated:#没有结束时,每一步,每次结束返回for循环
A = choose_action(S, q_table)#选择当前动作
S_, Reward = get_env_feedback(S, A)#获得行动反馈
q_predict = q_table.loc[S,A]#获取当前位置当前动作的Q值
#loc基于标签比如,loc[2,right]:第二行right列
if S_ != 'end':#如果下一步没有结束
q_target = Reward + GAMMA * q_table.iloc[S_, :].max()#Q Learning关键公式1
#下一步那一行中的最大值:下一步中最有价值的行动
else:#下一步就结束
q_target = Reward#可视奖励为Reward,1
is_terminated = True#结束,不再进入循环
q_table.loc[S, A] += ALPHA * (q_target - q_predict)#Q Learning关键公式2
#用学习率乘上目标与实际获得Q值的差值来更新当前位置当前动作的Q值
S = S_#站到下一步的位置
update_env(S, episode, step_counter+1)#没有结束时,每一步都更新环境
step_counter += 1#总步数计数器+1
return q_table#更新Q表
if __name__ == "__main__":
q_table = RL()
print('\rQ-table:')
print(q_table)#打印强化学习后更新出来的Q表
下面是我的一些思考:
从当前Q表(行为-价值表)中判断当前环境下可采取的最佳行为,如果未知最佳行为,则随机采取行为(这是环境影响行为)
采取行为时对环境造成影响(这是行为影响环境)
行为过后观察该动作对环境的影响和Q值变化和对以后行为的影响(这是行为影响行为)
如果单步调试观察会发现,从最开始的Q表全零,到第一次随机到正确答案,改变Q值,Q值是从后向前传递的,每次到达终点传递一次相当于以后每次走到这一步都可以按这次的步骤来走,不过这次随机出的结果不一定是“最佳结果”,因此需要一些“几率”(greedy)来找到更优解。
由此可以看出Q-learning的一些缺陷,比如在开荒的时候,太过于随机化,时间复杂度是2n级的,六个格子走到终点的概率是2-6=1/64,运气差一点可能上百步都走不到,更别说更大的世界更多的格子了。
还有一点要说的是,我认为在莫先生写的代码中,定义动作的那一部分其实是有一定问题的:
#在某个 state 地点, 选择行为
def choose_action(state, q_table):
state_actions = q_table.iloc[state, :] # 选出这个 state 的所有 action 值
if (np.random.uniform() > EPSILON) or (state_actions.all() == 0): # 非贪婪 or 或者这个 state 还没有探索过
action_name = np.random.choice(ACTIONS)
else:
action_name = state_actions.argmax() # 贪婪模式
return action_name
具体在if的判断条件,state_actions.all() == 0这里,在pandas的DataFrame定义中,官方文档pandas.DataFrame.all()说明 DataFrame.all()函数的作用是判断元素是否全为真,如果全为真(一个零都没有),则返回真(True);如果至少有一个为假(至少存在一个零),则返回假(False)。这个地方是判断“这个state有没有被探索过”,换句话说,在该state下是否每个动作的Q值都等于0。问题就在这里,这个函数本身是有返回值的,也就是这一段里面其实判断了两次,当 ‘left’ 和 ‘right’ 两个值一个为0,一个不为0时,该函数返回假(False),判断第一次;而接着又判断该返回值(False)和0是否相等,该 ‘==’ 判断返回的是真(True),因为假等于零,这是判断第二次。在这里,我们希望的是,当随机出的值大于greedy或者当’left’和’right’两个值都为0时,进入if后的执行语句。因此,这里使用pandas.DataFrame.all() == 0来判断是存在一定问题的。所以,我在这里进行了一定修改,想要 ‘left’ 和 ‘right’ 两个值都为0时返回真(True),根据这两个值的定义来看,他们都是大于等于零的数,当他们俩中的最大的值等于零时,这两个值都为0,因此我采用max(state_actions) == 0来实现。
首先,一维世界改造成二维世界要分以下几步:
1.思维的转换
2.Q表的改写
3.打印输入
其中Q表的改写是比较难想的,其他的思路都大致不变,总的来说只是考虑的因素多了一点,具体实施并不复杂。
先说思维的转换:
一般二维的世界都是通过矩阵来呈现坐标位置的,假如二维世界是10×10的空间,那么就应该创造一个10×10的矩阵来对应一个一个的位置。原本一维世界用来表示坐标的state不能在二维世界中继续使用了,要改写成二维坐标,我喜欢用x和y来表示。
再说Q表的改写:
首先 ‘left’,‘right’ 要改成四个动作 ‘up’,‘down’,‘left’,‘right’。其次是原本的Q表中纵轴是0、1、2、3…不能很好的适用于二维世界,因此我改为用矩阵来表示,将DataFrame的index值用矩阵来展示,但是世界矩阵不能直接用来当成index值,因为这个DataFrame本来就是一个列表的形式,纵列代表着世界坐标(用两个数字来表示这个格子处在世界矩阵的横坐标和纵坐标),横排代表着四个动作。因此,10×10的世界矩阵要转换成100×2的坐标矩阵(一个在世界中的格子要用两个数字来表示坐标)。这里可能不太好理解,不过慢慢想还是能够想通的。下面就是找规律转换坐标:
Q表坐标矩阵 | 世界矩阵坐标 |
---|---|
[0,0]=0 [0,1]=0 | [0,0] |
[1,0]=0 [1,1]=1 | [0,1] |
[2,0]=0 [2,1]=2 | [0,2] |
[3,0]=0 [3,1]=3 | [0,3] |
… | … |
[9,0]=0 [9,1]=9 | [0,9] |
[10,0]=1 [10,1]=0 | [1,0] |
[11,0]=1 [11,1]=1 | [1,1] |
… | … |
[99,0]=10 [99,1]=10 | [10,10] |
[ k , 0 ]=i [ k , 1 ]=j | [ i , j ] |
本应如此,但是DataFrame用矩阵填充index有点奇怪,它是把矩阵转置后填充的,因此还要转置一次。
def build_q_table(world_r,world_c,actions):#建立一个价值表,Q表
k = 0
I = np.zeros([world_r * world_c,2],int)
for i in range(world_r):
for j in range(world_c):
I[k,0] = i
I[k,1] = j
k+=1
I = np.transpose(I).tolist()#DataFrame用矩阵填充是把矩阵转置后填充的,因此要把矩阵转置一次
#矩阵不能直接填充,要把矩阵转换为list
table = pd.DataFrame(np.zeros((world_r * world_c, len(actions))),index=I ,columns=actions)
return table
Q表的改写就完成了,改写后的Q表表示为这样(全零时):
… | up | down | left | right |
---|---|---|---|---|
0 , 0 | 0.0 | 0.0 | 0.0 | 0.0 |
0 , 1 | 0.0 | 0.0 | 0.0 | 0.0 |
0 , 2 | 0.0 | 0.0 | 0.0 | 0.0 |
0 , 3 | 0.0 | 0.0 | 0.0 | 0.0 |
… | … | … | … | … |
9 , 8 | 0.0 | 0.0 | 0.0 | 0.0 |
9 , 9 | 0.0 | 0.0 | 0.0 | 0.0 |
[100 rows x 4 columns]
其中最左边的那一列就是我们所写的坐标矩阵,当然实际的Q表中并没有逗号,写上只是方便理解。
最后是打印输出,也就是环境更新:
其实也比较好理解,就是把每次打印一排一维世界改成了每次打印“世界行数”排一维世界。这里用for循环即可。
def update_env(pos_x,pos_y, episode, step_counter):#环境更新
if pos_x == 'end' and pos_y == 'end':#如果站在终点上
os.system("cls")#清屏
print('Episode %s: total_steps = %s' % (episode + 1, step_counter))
time.sleep(2)#等待
else:#如果没在终点
os.system("cls")#清屏
for i in range(WORLD_R):
env = ['+'] * (WORLD_C)
if i == end_pos_y:
env[end_pos_x] = 'O'
if i == pos_y:
env[pos_x] = '@'
a = ''.join(env)
print('{}'.format(a))
time.sleep(FRESH_TIME)#等待移动间隔时间
循环中每次到了当前所在位置的那一排,就改变当前序列,把“探险者”添加进去;每次到了出口的那一排,就改变当前序列,把“出口”加进去。
下面是全部代码,复制粘贴到编译器中运行即可看到最后效果:
import numpy as np
import pandas as pd
import time
import os
# +------>
# |
# |
# |
#\/
#改二维
np.random.seed(1)
WORLD_R = 10#二维世界行的数目
WORLD_C = 10#二维世界列的数目
ACTIONS = ['up','down','left', 'right']#探索者的可用动作,上下左右
EPSILON = 0.9#贪婪度greedy,95%的概率采取当前最优选择
ALPHA = 0.2#学习率
GAMMA = 0.9#可视奖励递减值
MAX_EPISODES = 20#最大回合数
FRESH_TIME = 0.1#移动间隔时间
end_pos_x = 8
end_pos_y = 9
def build_q_table(world_r,world_c,actions):#建立一个价值表,Q表
k = 0
I = np.zeros([world_r * world_c,2],int)
for i in range(world_r):
for j in range(world_c):
I[k,0] = i
I[k,1] = j
k+=1
I = np.transpose(I).tolist()#DataFrame用矩阵填充index太奇怪了,它是把矩阵转置后填充的,因此要把矩阵转置一次
table = pd.DataFrame(np.zeros((world_r * world_c, len(actions))),index=I ,columns=actions)
return table
def choose_action(pos_x,pos_y, q_table):#选择动作,根据位置选动作
pos_actions = q_table.loc[(pos_x,pos_y), :]
#[pos_x,pos_y, :]表示pos_x,pos_y那一行的所有可执行动作都列出来
if (np.random.rand() > EPSILON) or (max(pos_actions) == 0):#max最大值是0,则全为0
#从[0,1)中随机产生一个数,如果这个数大于EPSILON(0.9),或者如果Q表中state行两个数都等于0,那么执行下一步
action_name = np.random.choice(ACTIONS)#从四个动作中随机选择一个动作执行
else:
action_name = pos_actions.idxmax()#从Q表中当前位置的Q值中选择出最大值,得到它的索引
return action_name
def get_env_feedback(pos_x,pos_y, Action):#环境反馈
if Action == 'up':
if pos_y == end_pos_y + 1 and pos_x == end_pos_x:#下一个动作是终点
next_pos_x = 'end'
next_pos_y = 'end'
Reward = 1
elif pos_y == 0:#到顶了,无法继续up
next_pos_x = pos_x
next_pos_y = pos_y
Reward = 0
else:
next_pos_y = pos_y - 1#中间
next_pos_x = pos_x
Reward = 0
elif Action == 'down':
if pos_y == end_pos_y - 1 and pos_x == end_pos_x:#下一个动作是终点
next_pos_x = 'end'
next_pos_y = 'end'
Reward = 1
elif pos_y == WORLD_R - 1:#到底了,无法继续down
next_pos_x = pos_x
next_pos_y = pos_y
Reward = 0
else:
next_pos_y = pos_y + 1#中间
next_pos_x = pos_x
Reward = 0
elif Action == 'left':
if pos_x == end_pos_x + 1 and pos_y == end_pos_y:#下一个动作是终点
next_pos_x = 'end'
next_pos_y = 'end'
Reward = 1
elif pos_x == 0:#到最左了,无法继续left
next_pos_x = pos_x
next_pos_y = pos_y
Reward = 0
else:
next_pos_x = pos_x - 1#中间
next_pos_y = pos_y
Reward = 0
elif Action == 'right':
if pos_x == end_pos_x - 1 and pos_y == end_pos_y:#下一个动作是终点
next_pos_x = 'end'
next_pos_y = 'end'
Reward = 1
elif pos_x == WORLD_C - 1:#到最右了,无法继续right
next_pos_x = pos_x
next_pos_y = pos_y
Reward = 0
else:
next_pos_x = pos_x + 1#中间
next_pos_y = pos_y
Reward = 0
return next_pos_x, next_pos_y, Reward
def update_env(pos_x,pos_y, episode, step_counter):#环境更新
if pos_x == 'end' and pos_y == 'end':#如果站在终点上
os.system("cls")#清屏
print('Episode %s: total_steps = %s' % (episode + 1, step_counter))
time.sleep(2)#等待
else:#如果没在终点
os.system("cls")#清屏
for i in range(WORLD_R):
env = ['+'] * (WORLD_C)
if i == end_pos_y:
env[end_pos_x] = 'O'
if i == pos_y:
env[pos_x] = '@'
a = ''.join(env)
print('{}'.format(a))
time.sleep(FRESH_TIME)#等待移动间隔时间
def RL():#强化学习
q_table = build_q_table(WORLD_R,WORLD_C,ACTIONS)#实例化Q表
for episode in range(MAX_EPISODES):#在总回合数以内,每一回合
step_counter = 0#总步数计数器
pos_x = 0
pos_y = 0
is_terminated = False#是否结束
update_env(pos_x,pos_y, episode, step_counter)#初始环境
while not is_terminated:#没有结束时,每一步,每次结束返回for循环
Action = choose_action(pos_x,pos_y, q_table)#选择当前动作
next_pos_x,next_pos_y, Reward = get_env_feedback(pos_x,pos_y, Action)#获得行动反馈
q_predict = q_table.loc[(pos_x,pos_y),Action]#获取当前位置当前动作的Q值,loc通过标签检索
#loc基于标签比如,loc[2,right]:第二行right列
if next_pos_x != 'end' and next_pos_y != 'end':#如果下一步没有结束
q_target = Reward + GAMMA * q_table.loc[(next_pos_x,next_pos_y), :].max()#Q Learning关键公式1,iloc通过序号检索
#下一步那一行中的最大值:下一步中最有价值的行动
else:#下一步就结束
q_target = Reward#可视奖励为Reward,1
is_terminated = True#结束,不再进入循环
q_table.loc[(pos_x,pos_y), Action] += ALPHA * (q_target - q_predict)#Q Learning关键公式2
#用学习率乘上目标与实际获得Q值的差值来更新当前位置当前动作的Q值
pos_x = next_pos_x#站到下一步的位置
pos_y = next_pos_y
update_env(pos_x,pos_y, episode, step_counter + 1)#没有结束时,每一步都更新环境
step_counter += 1#总步数计数器+1
return q_table#更新Q表
if __name__ == "__main__":
q_table = RL()
os.system("cls")#清屏
print('Q-table:')
print(q_table)