计算机

RL 动态规划

(链接未公开) 是一类非常常见的算法。在 RL 中,我们可以使用 DP 来递推计算价值。

策略迭代

策略评估

使用如下迭代方程,可以通过迭代计算一个给定策略 \pi 的价值函数:

V_{k + 1}(s) = \sum_{a \in \mathcal{A}} \pi(a \, | \, s) \left[ r(s,a) + \gamma \sum_{s' \in \mathcal{S}} p(s' \, | \, s, a) V(s') \right].

上述方法进行一次迭代的时间复杂度是 O \left(|\mathcal{S}|^2 |\mathcal{A}| \right)。

策略提升

根据 策略提升定理,使用如下方法构造一个新的策略,就可以得到一个更好的策略:

\pi' (s) = \arg \max_{a \in \mathcal{A}} Q (s, a).

价值迭代

根据 Markov 决策过程的 价值提升迭代算子,对价值函数 V_k(s) 使用以下迭代可以逐渐计算出最优价值:

V_{k+1}(s) = \max_{a \in \mathcal{A}} \left[ r(s,a) + \gamma \sum_{s' \in \mathcal{S}} p(s' \, | \, s, a) V_k(s') \right];

进行一次价值迭代的时间复杂度是 O \left(|\mathcal{S}|^2 |\mathcal{A}| \right),对于转移概率已知并且情形较小的 RL 问题可以接受。

求得最优价值 V^* 后,使用如下方法得到最优策略:

\pi^*(s) \in \arg \max_{a \in \mathcal{A}} \left[ r(s,a) + \gamma \sum_{s' \in \mathcal{S}} p(s' \, | \, s, a) V^*(s') \right].

以上公式的含义是:\pi^*(a \, | \, s) 中,概率非 0 的 a 只能在 \arg \max_{a \in \mathcal{A}} 中取得,对于不能取得最大值的动作 a',令 \pi^*(a' \, | \, s) = 0。可以直接将 \pi^* 实现为一个确定性策略,任取一个取得最大价值的动作。

应用

走迷宫问题

出口或陷阱的设置:此时通常设置这个状态为吸收态。

奖励的获取可能和下一个状态有关,此时要注意状态价值的定义是可获得的收益的期望,可能需要再代码中手动计算。

冰湖问题

冰湖(frozen lake)是走迷宫问题的变形,此时一个行动并不确定性地到达目标位置,而是以一定概率到达。

冰湖的环境代码如下。这里通过设置额外的吸收态 self.absorption,使得到达终点或者落入湖中(陷阱)的智能体不可能再返回,也不可能再获得奖励。

class FrozenLake:
    def __init__(self, nrow: int, ncol: int, holes: list[int]):
        self.nrow = nrow
        self.ncol = ncol
        self.n_states = nrow * ncol + 1
        # 0 for a empty slot, 1 for the terminal, -1 for a hole, 2 for an absorption
        self.states = np.zeros(self.n_states, dtype=np.int32)
        self.states[holes] = -1
        self.states[self.n_states - 2] = 1
        self.states[self.n_states - 1] = 2
        self.absorption = self.n_states - 1
        # for action: 0 for up, 1 for down, 2 for left, 3 for right.
        self.n_actions = 4
        # Initialize transition matrix and reward
        self.transition = np.zeros(
            (self.n_states, self.n_actions, self.n_states), dtype=np.float64
        )
        self.reward = np.zeros(
            (self.n_states, self.n_actions), dtype=np.float64
        )
        for action in range(self.n_actions):
            self.transition[self.absorption, action, self.absorption] = 1
        
        for x in range(nrow):
            for y in range(ncol):
                state = x * ncol + y
                if self.states[state] != 0:
                    if self.states[state] == -1:
                        reward = -100
                    elif self.states[state] == 1:
                        reward = 1000
                    # Terminate
                    for action in range(self.n_actions):
                        self.transition[state, action, self.absorption] = 1
                        self.reward[state, action] = reward
                    continue
                can_up = can_down = can_left = can_right = True
                if x == 0:
                    can_up = False
                if x == nrow - 1:
                    can_down = False
                if y == 0:
                    can_left = False
                if y == ncol - 1:
                    can_right = False
                for action in range(self.n_actions):
                    if can_up and action != 1:
                        next_state = (x - 1) * ncol + y
                        self.transition[state, action, next_state] = 1
                    if can_down and action != 0:
                        next_state = (x + 1) * ncol + y
                        self.transition[state, action, next_state] = 1
                    if can_left and action != 3:
                        next_state = x * ncol + y - 1
                        self.transition[state, action, next_state] = 1
                    if can_right and action != 2:
                        next_state = x * ncol + y + 1
                        self.transition[state, action, next_state] = 1
                    if (s := np.sum(self.transition[state, action])) != 0:
                        self.transition[state, action] *= 1 / s
    
    def step(self, state: int, action: int) -> tuple[int, float, bool]:
        """
        return (state, reward, is_end)
        """
        reward = self.reward[state, action]
        if self.states[state] != 0:
            return (-1, reward, True)
        distribution = self.transition[state, action]
        next_state = random.choices(np.arange(self.n_states), distribution)
        return (next_state, reward, False)

依据两个公式,使用 DP 可以获得冰壶问题的最优状态和策略:

v_state = np.zeros(env.n_states, dtype=np.float64)
v_state_old = np.zeros_like(v_state)
gamma = 0.95
for k in tqdm(range(10000)):
    v_state_old, v_state = v_state, v_state_old
    for state in range(env.n_states):
        v_state[state] = np.max([
            env.reward[state, action] + gamma * np.sum([
                env.transition[state, action, next_state] * v_state_old[next_state] 
                for next_state in range(env.n_states)
            ])
            for action in range(env.n_actions)
        ])

policy = np.zeros(env.n_states, dtype=np.int32)
for state in range(env.n_states):
    policy[state] = np.argmax([
        env.reward[state, action] + gamma * np.sum([
            env.transition[state, action, next_state] * v_state[next_state]
            for next_state in range(env.n_states)
        ])
        for action in range(env.n_actions)
    ])

np.array(list(map(lambda x: "^v<>"[x], policy)))[:-1].reshape(4, 4)

DP 的缺陷

使用 DP 求解 RL 时,要求我们已知转移概率 p(s' \, | \, s, a),并且状态集合 \mathcal{S} 和动作集合 \mathcal{A} 都是取值较小的有限集。这在大多数情形下都是不可能的,特别是当引入神经网络时。许多强化学习的实际任务只能基于多次采样获得的轨迹来估计概率。

BEYOND THE ARTICLE

读完了,有什么想法?

阅读已保存的问答示例,并循着引用回到原文。

查看公开问答示例
— 本文完 — 返回文章列表