基于函数逼近的同轨策略控制
上一章节我们已经学习了基于函数逼近的价值函数的迭代的过程,转换到策略的控制问题是直接的,直接使用Q值来代替V值进行计算很自然。
但是上一章节其实一直有个问题,我没有都没有解决,而且还很顺利的完成了整个章节的讲解,就很神奇,就是上一个章节的动作选取的π(s)\pi(s)π(s)具体是如何实现的?我们一,不一定有s的转移概率,二,我们也不一定有s,a的pr的转移矩阵和收益函数。
换句话说,就算我们知道了一个确切的价值函数的函数逼近表达形式,我们依然没有办法来指导我们如何进行策略控制,你不知道如何从s执行什么动作能到我们的期望的argmax的s’
1. 引言
基于上面的问题,我们使用的默认会使用到q(s,a)的表达形式来进行选择,但是这里有个新的问题,比如s是
2. 回顾价值函数逼近
在策略评估中,我们的目标是近似价值函数 vπ(s)v_π(s)vπ(s) 或动作价值函数 qπ(s,a)q_π(s,a)qπ(s,a)。使用参数化函数:
v^(s,w)≈vπ(s)\hat{v}(s,\mathbf{w}) ≈ v_π(s)v^(s,w)≈vπ(s)
q^(s,a,w)≈qπ(s,a)\hat{q}(s,a,\mathbf{w}) ≈ q_π(s,a)q^(s,a,w)≈qπ(s,a)
其中 w\mathbf{w}w 是权重向量。
3. 半梯度方法
在同轨策略学习中,我们使用半梯度方法来更新参数,半梯度的方法是因为下面的式子中的U实际上也是一个跟w相关的参数,但是我们的在考虑梯度的时候不会考虑这个点。基本更新规则是:
wt+1=wt+α[Ut−q^(St,At,wt)]∇q^(St,At,wt)\mathbf{w}_{t+1} = \mathbf{w}_t + \alpha[U_t - \hat{q}(S_t,A_t,\mathbf{w}_t)]\nabla\hat{q}(S_t,A_t,\mathbf{w}_t)wt+1=wt+α[Ut−q^(St,At,wt)]∇q^(St,At,wt)
其中:
- UtU_tUt 是目标值
- α\alphaα 是步长
- ∇q^\nabla\hat{q}∇q^ 是关于参数的梯度
4. SARSA算法与函数逼近
4.1 算法描述
初始化 w
对每个回合:
初始化 S
选择 A 从 S using ε-greedy policy
对每一步:
执行动作A,观察 R, S'
选择 A' 从 S' using ε-greedy policy
w ← w + α[R + γq̂(S',A',w) - q̂(S,A,w)]∇q̂(S,A,w)
S ← S'; A ← A'
直到S为终止状态
4.2 线性函数逼近
最常用的函数逼近形式是线性形式:
q^(s,a,w)=wTx(s,a)=∑iwixi(s,a)\hat{q}(s,a,\mathbf{w}) = \mathbf{w}^T\mathbf{x}(s,a) = \sum_i w_i x_i(s,a)q^(s,a,w)=wTx(s,a)=∑iwixi(s,a)
其中:
- x(s,a)\mathbf{x}(s,a)x(s,a) 是特征向量
- wiw_iwi 是对应的权重
4.3 高山行车的问题解
这个是使用10步的sarsa来进行更新的学习的过程,不过我的奖励函数可能设置的有问题,导致他的整体是在学习
我使用了了10步sarsa的方式学习了max step = 1000的tills的分类,talk is cheap ,show me the code
这里好几个问题,
- 使用了hash来实现tile to featrue. hash比总的tile要大,是因为要解决hash冲突的问题
- tiles的层数是8,对于连续的输入变量是需要分成8层的,每层8个tile
- 但是对于离散的aciton,就是3个变量
- 最后的成果来看,我们可以稳定在160左右的步骤可以到达目的地,可以认为是稳定的解决了问题,下面是测试的最后的几轮的输出,算是比较稳定的收敛在这个值了
Episode 9985/10000, Total Reward: -159.0
Episode 9986/10000, Total Reward: -177.0
Episode 9987/10000, Total Reward: -163.0
Episode 9988/10000, Total Reward: -178.0
Episode 9989/10000, Total Reward: -152.0
Episode 9990/10000, Total Reward: -174.0
Episode 9991/10000, Total Reward: -175.0
Episode 9992/10000, Total Reward: -178.0
Episode 9993/10000, Total Reward: -179.0
Episode 9994/10000, Total Reward: -152.0
Episode 9995/10000, Total Reward: -178.0
Episode 9996/10000, Total Reward: -178.0
Episode 9997/10000, Total Reward: -154.0
Episode 9998/10000, Total Reward: -178.0
Episode 9999/10000, Total Reward: -158.0
Episode 10000/10000, Total Reward: -159.0
import gymnasium as gym
import numpy as np
import matplotlib.pyplot as plt
class IHT:
"""
IHT stands for Index Hash Table, which provides an index lookup mechanism
for tile coding.
"""
def __init__(self, size_val):
self.size = size_val
self.overfull_count = 0
self.dictionary = {}
def get_index(self, obj, read_only=False):
"""Returns index for given obj. If not in table, adds it if read_only is False."""
d = self.dictionary
if obj in d:
return d[obj]
if read_only:
return None
size = self.size
count = len(d)
if count >= size:
if self.overfull_count == 0:
print('IHT full, starting to allow collisions')
self.overfull_count += 1
return hash(obj) % self.size
d[obj] = count
return count
#要是写不出来,就说明我不是真正的理解了这件事
#tile只是返回feature,不涉及更新
class TileCoding:
def __init__(self, num_tilings=8, num_tiles=8, num_inputs=2):
# 初始化哈希表
self.iht = IHT(4096)
# 其他初始化...
self.num_tilings = num_tilings
self.num_tiles = num_tiles
# 设置状态空间范围
self.position_min = -1.2
self.position_max = 0.6
self.velocity_min = -0.07
self.velocity_max = 0.07
# 计算每个维度的比例
self.position_scale = (self.position_max - self.position_min) / self.num_tiles
self.velocity_scale = (self.velocity_max - self.velocity_min) / self.num_tiles
# 使用不同的偏移比例
self.position_offset = self.position_scale / self.num_tilings
self.velocity_offset = self.velocity_scale / self.num_tilings
def get_features(self, position, velocity, action):
active_tiles = []
# 对每一层使用不同的偏移
for tiling in range(self.num_tilings):
# 1. 计算非对称偏移
position_shift = tiling * self.position_offset
velocity_shift = (tiling * (tiling + 1) / 2) * self.velocity_offset # 非线性偏移
# 2. 计算离散索引
position_idx = int((position - self.position_min + position_shift) / self.position_scale)
velocity_idx = int((velocity - self.velocity_min + velocity_shift) / self.velocity_scale)
# 3. 使用哈希表获取唯一索引
index = self.iht.get_index((position_idx, velocity_idx, action, tiling))
# 4. 添加到激活tile列表
active_tiles.append(index)
#将激活的tile转换为特征向量
resultTiles = np.zeros(self.iht.size)
for tile in active_tiles:
resultTiles[tile] = 1
return resultTiles
#使用函数模拟,不使用神经网络,也不使用Q-learning
class NStepSarsa:
def __init__(self, env, num_tilings, num_tiles, num_inputs, gamma=0.95, alpha=0.5/8, epsilon=0.1, n_step=10):
self.env = env
self.tile_coding = TileCoding(num_tilings, num_tiles, num_inputs)
self.gamma = gamma
self.alpha = alpha
self.epsilon = epsilon
self.n_step = n_step
self.weights = np.zeros(self.tile_coding.iht.size)
self.state_list = []
self.action_list = []
self.reward_list = []
self.next_state_list = []
self.next_action_list = []
def get_value(self, state, action):
feature = self.tile_coding.get_features(state[0], state[1], action)
return np.dot(feature, self.weights)
def get_action(self, state):
#使用epsilon贪心策略
#epsilon = 0.1
#如果随机数小于epsilon,则随机选择一个动作
#否则,选择Q值最大的动作
max_q_value = -float('inf')
if np.random.random() < self.epsilon:
return self.env.action_space.sample()
else:
#返回得到最大Q值的动作
#注意,q = w * x(s,a)
#x(s,a)是特征向量,w是权重向量
#q值最大的动作,就是特征向量与权重向量点积最大的动作
for action in range(self.env.action_space.n):
q_value = self.get_value(state, action)
if q_value > max_q_value:
max_q_value = q_value
best_action = action
return best_action
def herusticReward(self, state):
#启发式的奖励函数
#这个是启发式的奖励函数,不是Q-learning的奖励函数
#-0.52是谷底位置
return abs(state[1]) + abs(state[0]+0.52)
def update(self, state, action,done, reward, next_state, next_action):
#更新当前的state,action,reward,next_state,next_action
self.state_list.append(state)
self.action_list.append(action)
self.reward_list.append(reward)
self.next_state_list.append(next_state)
self.next_action_list.append(next_action)
#增量更更新
reward_sum = 0
if len(self.state_list) == self.n_step or done:
for i in range(len(self.state_list)):
reward_sum += self.reward_list[i]*self.gamma**i
#剩余价值
if not done:
reward_sum += self.gamma**self.n_step * self.get_value(self.next_state_list[-1], self.next_action_list[-1])
#计算完毕以后,移除
self.state_list.pop(0)
self.action_list.pop(0)
self.reward_list.pop(0)
self.next_state_list.pop(0)
self.next_action_list.pop(0)
#更新权重
feature = self.tile_coding.get_features(self.state_list[0][0], self.state_list[0][1], self.action_list[0])
self.weights += self.alpha * (reward_sum - self.get_value(self.state_list[0], self.action_list[0])) * feature
def train(env, agent, num_episodes, max_steps):
for i in range(num_episodes):
state,_ = env.reset()
action = agent.get_action(state)
total_reward = 0
for t in range(max_steps):
next_state, reward, done, _, _ = env.step(action)
next_action = agent.get_action(next_state)
total_reward += reward
#启发式的reward
reward += agent.herusticReward(next_state)
agent.update(state, action, done, reward, next_state, next_action)
state = next_state
action = next_action
if done:
break
print(f"Episode {i+1}/{num_episodes}, Total Reward: {total_reward}")
if (i+1) % 10 == 0:
agent.epsilon *= 0.95
def test(env, agent, num_episodes, max_steps):
for i in range(num_episodes):
state = env.reset()
total_reward = 0
for t in range(max_steps):
action = agent.get_action(state)
next_state, reward, done, _, _ = env.step(action)
total_reward += reward
state = next_state
if done:
break
print(f"Episode {i+1}/{num_episodes}, Total Reward: {total_reward}")
def main():
env = gym.make("MountainCar-v0")
agent = NStepSarsa(env, num_tilings=8, num_tiles=8, num_inputs=2, gamma=0.95, alpha=0.5/8, epsilon=0.1, n_step=10)
train(env, agent, num_episodes=10000, max_steps=1000)
test(env, agent, num_episodes=10, max_steps=200)
if __name__ == "__main__":
main()
5 半梯度n步Sarsa
上面的代码已经实现了相关的内容,基本上来说和之前一样,只是计算的结果是过去n步的一个折扣收益,但是我的这个代码的收敛速度灭有原文中的那么快,如果有人知道为什么,可以跟我分析一下
6 平均收益
平均性收益的问题主要是用于解决连续性的回报的问题,放弃了使用gamma而是使用平均收益作为一个值,但是这里需要注意的是,原文中的公式r(π)r(\pi)r(π)计算的是所有的s的平均收益
原文中又一次提到了稳定态的概念,对于一个u(s)如果是稳态的,那么乘以他的转移矩阵以后,应该和自己相等,也就是前面的PU = U
平均收益的公式也很简单,在前面的基础上,差分
δ=Rt+1−Rˉt+1+v^(st+1,w)−(^s,w)\delta = R_{t+1} - \bar R_{t+1} +\hat v(s_{t+1},w) - \hat(s,w)δ=Rt+1−Rˉt+1+v^(st+1,w)−(^s,w)
如果是sarsa的格式,那么显然使用的Q来代替V,没有本质的区别
7 弃用折扣
为什么平均收益可以弃用折扣,原文中给出一个例子就是一个灭有开头的没有结尾的收益序列,此时的S并不能用来区分实际的状态,换句话说就是之前的完全不知道当前是哪以及当前的转移的概率的情况,对于性能的评估,我们可以选取一段时间的平均收益作为性能指标
但是为什么可以弃用r,是实际上,带折扣的平均收益 = 1/(1-r) 平均收益,这个证明的过程我不放出来了,本质上是一回事,就是访问的时候,带不带折扣和他的位置的关系,前后的关系其实意义不大,实际上每个s的收益都在各个位置上出现,带γ\gammaγ的收益本质上是平均收益的一扩展
8 差分半梯度n步sarsa
很显然,既然有1步的平均收益,那是不是就有n步的,我们需要做的是就是保存每个时刻的R的平均收益,这样就可以计算n步的平均收益情况的下的差分值,用于更新我们的权重值
得到我们的更新公式
δ=∑τ+1τ+n(Ri−Rˉi)+q(St+n,At+n,w)−q(Sτ,Aτ,w)\delta = \sum_{\tau+1}^{\tau+n}(R_i - \bar R_i) + q(S_{t+n},A_{t+n},w) - q(S_\tau,A_\tau,w)δ=∑τ+1τ+n(Ri−Rˉi)+q(St+n,At+n,w)−q(Sτ,Aτ,w)
然后就是w的权重更新,不列出来了
8. 示例代码
class LinearSARSA:
def __init__(self, n_features, alpha=0.1, gamma=0.99, epsilon=0.1):
self.w = np.zeros(n_features)
self.alpha = alpha
self.gamma = gamma
self.epsilon = epsilon
def predict(self, features):
return np.dot(self.w, features)
def update(self, features, reward, next_features):
current_q = self.predict(features)
next_q = self.predict(next_features)
target = reward + self.gamma * next_q
self.w += self.alpha * (target - current_q) * features