0x01 价值迭代算法基础概念

0x01.1 奖励

若要实现价值迭代,首先要定义价值,在迷宫任务中,到达目标将获得奖励。

  • 特定时间t给出奖励Rt称为即时奖励
  • 未来获得的奖励总和Gt被称为总奖励
  • Gt=R(t+1)+R(t+2)+R(t+3)
  • 考虑时间因素,需要引入折扣率,这样可以在最后拟合时获得时间最短的策略。
  • Gt=R(t+1)+yR(t+2)+y^2R(t+3)....

0x02 动作价值与状态价值

在迷宫中,当我们的智能体走到终点时设置奖励R(t+1)=1

0x02.1 动作价值

强化学习入门之智能走迷宫-价值迭代算法-LMLPHP
如果状态s=S7且动作a=向右,则意味着S7→S8移动,这样就可以在下一步中达到目标并获得奖励Rt+1=1。

动作价值可以用动作价值函数Qπ(s,a)表示。有4种类型的动作(向上、向右、向下、向左),在动作索引为a=1时向右移动,所以有:
Qπ(s=7,a=1)=Rt+1=1

若此时在S7的智能体下一步动作是向上,那么S7->S4 远离了目标,这样要想最快到达终点需要啊 S7->S4->S7->S8 可以看到S7会重复2次
此时获得的奖励就被打了折扣
Qπ(s=7,a=0)=γ2*1
依此类推,如果方向一直不对,那么奖励就一直被打折扣,也就越来越少。
这里因为奖励是根据智能体的动作变化而变化的,所以被称为动作价值。

0x02.2 状态价值

状态价值是指在状态s下遵从策略π行动时,预计在将来获得的总奖励Gt。将状态s的状态价值函数写为Vπ(s)

若智能体在S7状态,向右移动即可获得奖励 存在 Vπ(s=7)=1

若智能体在S4,向下移动到S7,再向右移动到S8, 存在 Vπ(s=4)=y1
也可表示为 Vπ(s=4)=R(t+1)+y
Vπ(s=7)=0+y*1=y

0x03 贝尔曼方程和马尔可夫决策过程

状态价值函数最后可通过这个方程表达 -》 强化学习入门之智能走迷宫-价值迭代算法-LMLPHP
这个方程被称为贝尔曼方程

  • VΠ(s) 表示在状态s时的状态价值V

  • 该状态价值是通过右侧具有最大值动作的期望的价值,如果我们想要拟合一个最大的价值状态,那么在迷宫中,最短路径就能实现最大价值期望。

  • R(s'a) 是在状态s下采用动作a移动后的新状态的即时奖励R(t+1)

  • VΠ()中的s(s,a)表示在状态s下采用动作a移动后的新状态s+1

  • 方程表达的是 新状态的状态价值V时间折扣率加上现在的即使奖励的和的最大值就是当前的状态价值。
    举个例子,比如在S8的时候设置了即时奖励 1 ,所以根据公式,在S7的时候 状态价值就是 1
    而在S4时 状态价值是 0+1
    y
    在S3时, 状态价值是 0+1yy
    方向用概率表示, 从S3->S4->S7->S8 假设方向为a 上右下左随机的概率 a11y^2+a21y+a31 可以得到最大价值期望。
    **如果我们可以通过一个函数使得 a1
    1y^2+a21y+a31 收敛于一个最大值, 就能不断改变a的概率, 使的a1中向右概率大大增加,a2中向下概率大大增加,a3中向右概率大大增加。**

作为贝尔曼方程成立的前提条件,学习对象必须是满足马尔可夫决策过程的,即下一步的状态由当前状态和采用的动作决定

0x04 使用Sarsa算法与epsilon贪婪法实现策略

epsilon贪婪法 简单理解就是 以 一定概率p随机行动, 以剩下的1-p的概率采用动作价值Q最大的行动。 随着实验次数的增加,p的概率会减小,原因是不管怎么走都会走一条固定的最短路径到达终点。

由于初始状态不清楚每个状态的动作价值 所以需要随机定义

[a,b]=theta_0.shape # 获取行,列数
Q=np.random.rand(a,b)*theta_0 # 将theta_0乘到各个元素上,使Q墙壁方向为nan

然后定义随机方向策略

def simple_convert_into_pi_from_theta(theta):
    ''' 简单计算比率'''
    [m,n]=theta.shape # 读取theta矩阵
    pi=np.zeros((m,n))
    for i in range(0,m):
        pi[i,:]=theta[i,:]/np.nansum(theta[i,:]) # 计算比率
    pi=np.nan_to_num(pi) # 将nan转换为0
    return pi

定义epsilon贪婪算法,使得一部分随机走,一部分按照求最大价值函数Q的方向去走

# 实现epsilon贪婪算法
def get_action(s,Q,epsilon,pi_0):
    direction=["up","right","down","left"]
    # 确定行动
    if np.random.rand()<epsilon:
        next_direction=np.random.choice()
    else:
        # 采用让Q获得最大值的动作
        next_direction=direction[np.nanargmax(Q[s,:])]

    # 为每个动作设置索引
    if next_direction=="up":
        action=0
    if next_direction=="right":
        action=1
    if next_direction=="down":
        adcion=2
    if next_direction=="left":
        action=3
    return action
# 设置状态索引
def get_s_next(s,a,Q,epsilon,pi_0):
    direction = ["up", "right", "down", "left"]
    next_direction=direction[a] # 动作a的方向
    # 根据动作确定下一步状态
    if next_direction=='up':
        s_next=s-3 # 向上移动 状态数-3
    if next_direction=="right":
        s_next = s + 1
    if next_direction=="down":
        s_next = s + 3
    if next_direction=="left":
        s_next = s - 1
    return s_next

如果获得动作价值函数Q(s,a)的正确值,则贝尔曼方程
Q(st,at)=Rt+1+γQ(st+1,at+1)
所表示的关系成立。
然而,由于在学习过程中尚未正确求得动作价值函数,因此该等式是不成立的。
此时,上述等式两边之间的差Rt+1+γQ(st+1,at+1)-Q(st,at)是TD误差(时间差,Temporal Difference error)。如果此时TD误差为0,则表示已正确学习到了动作价值函数。Q的更新公式是:
Q(st,at)=Q(st,at)+η*(Rt+1+γQ(st+1,at+1)-Q(st,at)
其中η是学习率,η后面是TD误差。遵循此更新公式的算法称为Sarsa算法

基于Sarsa算法去更新策略

def Sarsa(s,a,r,s_next,a_next,Q,eta,gamma):
    if s_next==8:
        Q[s,a]=Q[s,a]+eta*(r-Q[s,a])
    else:
        Q[s,a]=Q[s,a]+eta*(r+gamma*Q[s_next,a_next]-Q[s,a])
    return Q

通过该算法去求解

def goal_maze_ret_s_a_Q(Q,epsilon,eta,gamma,pi):
    s=0
    a=a_next=get_action(s,Q,epsilon,pi)
    s_a_history=[[0,np.nan]] # 记录移动体序列
    while (1):
        a=a_next # 动作更新
        s_a_history[-1][-1]=a
        # 将动作放在当前状态
        s_next=get_s_next(s,a,Q,epsilon,pi)
        # 有效的下一个状态
        s_a_history.append([s_next,np.nan])
        # 代入下一个状态 动作未知则为nan
        if s_next==8:
            r=1 # 给奖励
            a_next=np.nan
        else:
            r=0
            a_next=get_action(s_next,Q,epsilon,pi)
            # 求得下一个动作
        # 更新价值函数
        Q=Sarsa(s,a,r,s_next,a_next,Q,eta,gamma)

        # 终止判断
        if s_next==8:
            break
        else:
            s=s_next
    return [s_a_history,Q]

设置初始值

# 求解
    eta=0.1 # 学习率
    gamma=0.9 # 时间折扣率
    epsilon=0.5 # epsilon贪婪算法
    v=np.nanargmax(Q,axis=1) # 根据 状态求最大价值
    is_continue=True
    episode=1
    while is_continue:
        print("当前回合:",str(episode))
        # epsilon贪婪法的值变小
        epsilon=epsilon/2
        # 通过Sarsa求解迷宫问题
        [s_a_history,Q]=goal_maze_ret_s_a_Q(Q,epsilon,eta,gamma,pi_0)
        # 状态价值变化
        new_v=np.nanmax(Q,axis=1) # 各状态求最大价值
        print(np.sum(np.abs(new_v-v))) # 输出状态价值变化
        v=new_v
        print("求解迷宫问题所需步骤:",str(len(s_a_history)-1))
        episode=episode+1
        if episode>50:
            break

完整代码

# 引入库函数
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import animation
# 画图
def plot():
    fig=plt.figure(figsize=(5,5))
    ax=plt.gca()
    # 画墙壁
    plt.plot([1,1],[0,1],color='red',linewidth=3)
    plt.plot([1,2],[2,2],color='red',linewidth=2)
    plt.plot([2,2],[2,1],color='red',linewidth=2)
    plt.plot([2,3],[1,1],color='red',linewidth=2)
    # 画状态
    plt.text(0.5,2.5,'S0',size=14,ha='center')
    plt.text(1.5,2.5,'S1',size=14,ha='center')
    plt.text(2.5,2.5,'S2',size=14,ha='center')
    plt.text(0.5,1.5,'S3',size=14,ha='center')
    plt.text(1.5,1.5,'S4',size=14,ha='center')
    plt.text(2.5,1.5,'S5',size=14,ha='center')
    plt.text(0.5,0.5,'S6',size=14,ha='center')
    plt.text(1.5,0.5,'S7',size=14,ha='center')
    plt.text(2.5,0.5,'S8',size=14,ha='center')
    plt.text(0.5,2.5,'S0',size=14,ha='center')
    plt.text(0.5,2.3,'START',ha='center')
    plt.text(2.5,0.3,'END',ha='center')
    # 设置画图范围
    ax.set_xlim(0,3)
    ax.set_ylim(0,3)
    plt.tick_params(axis='both',which='both',bottom='off',top='off',labelbottom='off',right='off',left='off',labelleft='off')
    # 当前位置S0用绿色圆圈
    line,=ax.plot([0.5],[2.5],marker="o",color='g',markersize=60)
    # 显示图
    plt.show()
def simple_convert_into_pi_from_theta(theta):
    ''' 简单计算比率'''
    [m,n]=theta.shape # 读取theta矩阵
    pi=np.zeros((m,n))
    for i in range(0,m):
        pi[i,:]=theta[i,:]/np.nansum(theta[i,:]) # 计算比率
    pi=np.nan_to_num(pi) # 将nan转换为0
    return pi
# 实现epsilon贪婪算法
def get_action(s,Q,epsilon,pi_0):
    direction=["up","right","down","left"]
    # 确定行动
    if np.random.rand()<epsilon:
        next_direction=np.random.choice(direction,p=pi_0[s,:])
    else:
        # 采用让Q获得最大值的动作
        next_direction=direction[np.nanargmax(Q[s,:])]

    # 为每个动作设置索引
    if next_direction=="up":
        action=0
    if next_direction=="right":
        action=1
    if next_direction=="down":
        action=2
    if next_direction=="left":
        action=3
    return action
# 设置状态索引
def get_s_next(s,a,Q,epsilon,pi_0):
    direction = ["up", "right", "down", "left"]
    next_direction=direction[a] # 动作a的方向
    # 根据动作确定下一步状态
    if next_direction=='up':
        s_next=s-3 # 向上移动 状态数-3
    if next_direction=="right":
        s_next = s + 1
    if next_direction=="down":
        s_next = s + 3
    if next_direction=="left":
        s_next = s - 1
    return s_next

# Sarsa算法更新策略
def Sarsa(s,a,r,s_next,a_next,Q,eta,gamma):
    if s_next==8:
        Q[s,a]=Q[s,a]+eta*(r-Q[s,a])
    else:
        Q[s,a]=Q[s,a]+eta*(r+gamma*Q[s_next,a_next]-Q[s,a])
    return Q

# 使用Sarsa算法求解迷宫问题
def goal_maze_ret_s_a_Q(Q,epsilon,eta,gamma,pi):
    s=0
    a=a_next=get_action(s,Q,epsilon,pi)
    s_a_history=[[0,np.nan]] # 记录移动体序列
    while (1):
        a=a_next # 动作更新
        s_a_history[-1][-1]=a
        # 将动作放在当前状态
        s_next=get_s_next(s,a,Q,epsilon,pi)
        # 有效的下一个状态
        s_a_history.append([s_next,np.nan])
        # 代入下一个状态 动作未知则为nan
        if s_next==8:
            r=1 # 给奖励
            a_next=np.nan
        else:
            r=0
            a_next=get_action(s_next,Q,epsilon,pi)
            # 求得下一个动作
        # 更新价值函数
        Q=Sarsa(s,a,r,s_next,a_next,Q,eta,gamma)

        # 终止判断
        if s_next==8:
            break
        else:
            s=s_next
    return [s_a_history,Q]



if __name__=="__main__":
    theta_0=np.array([[np.nan,1,1,np.nan], #S0
                      [np.nan,1,np.nan,1], #S1
                      [np.nan,np.nan,1,1], #S2
                      [1,1,1,np.nan], #S3
                      [np.nan,np.nan,1,1], #S4
                      [1,np.nan,np.nan,np.nan], #S5
                      [1,np.nan,np.nan,np.nan], #S6
                      [1,1,np.nan,np.nan],  #S7
                      ])   # S8位目标 不需要策略
    print(theta_0)
    #plot()
    # 设置初始的动作价值函数
    [a,b]=theta_0.shape # 获取行,列数
    Q=np.random.rand(a,b)*theta_0 # 将theta_0乘到各个元素上,使Q墙壁方向为nan
    pi_0=simple_convert_into_pi_from_theta(theta_0) # 设置移动方向初始策略
    # 求解
    eta=0.1 # 学习率
    gamma=0.9 # 时间折扣率
    epsilon=0.5 # epsilon贪婪算法
    v=np.nanargmax(Q,axis=1) # 根据 状态求最大价值
    is_continue=True
    episode=1
    while is_continue:
        print("当前回合:",str(episode))
        # epsilon贪婪法的值变小
        epsilon=epsilon/2
        # 通过Sarsa求解迷宫问题
        [s_a_history,Q]=goal_maze_ret_s_a_Q(Q,epsilon,eta,gamma,pi_0)
        # 状态价值变化
        new_v=np.nanmax(Q,axis=1) # 各状态求最大价值
        print(np.sum(np.abs(new_v-v))) # 输出状态价值变化
        v=new_v
        print("求解迷宫问题所需步骤:",str(len(s_a_history)-1))
        episode=episode+1
        if episode>50:
            break

运行效果
强化学习入门之智能走迷宫-价值迭代算法-LMLPHP

06-09 22:14