一、 策略迭代的执行流程
策略迭代的核心是一个"左脚踩右脚"螺旋上升的过程,通过不断循环交替进行"策略评估"和"策略提升",直至策略不再发生改变。
-
初始化: 设定一个初始策略(如上下左右概率各 25% 的均匀随机策略),并将所有状态的初始价值设为 0。
-
策略评估 (Policy Evaluation): 在给定当前策略的前提下,利用贝尔曼期望方程反复迭代,计算出该策略在所有状态下的真实期望回报(即状态价值函数)。在实际代码中,为了节省算力,当状态价值的最大变化差值小于设定的极小阈值(如 0.001)时,即可认为评估收敛并提前结束该步骤。
-
策略提升 (Policy Improvement): 根据"策略评估"阶段算出的最新状态价值,计算出在当前状态下采取每一个合法动作能带来的"动作价值"。然后,直接贪心地选择能带来最大动作价值的动作,作为新策略在该状态下的唯一选择。
-
收敛判断与循环: 检查经过提升后得到的新策略,是否与前一轮的旧策略完全一模一样。如果一模一样,说明策略已经到达巅峰无法再优化,算法停止并输出最优策略;如果不同,则用新策略替换旧策略,回到第 2 步继续"评估"。
二、 策略迭代的优势
-
宏观收敛极快(迭代轮数少): 虽然它的内部计算复杂,但从整体策略更新的次数来看,策略迭代通常只需要极少的大循环就能找到最优解。例如在悬崖漫步(Cliff Walking)环境中,智能体仅仅经历了 5 次"评估-提升"的交替循环,策略就完全收敛了。
-
理论严谨,必定最优: 基于策略提升定理,每一次更新产生的新策略必定优于(或等于)旧策略。只要在有限的 MDP 中,它能提供数学上严谨的绝对精确解。
三、 策略迭代的劣势
-
单次循环的计算代价极大: 在每一轮大循环中,单单是"策略评估"这一步就需要不断做贝尔曼期望方程迭代,直至价值收敛,这会耗费非常大的计算代价。相比之下,价值迭代(Value Iteration)直接把这两步融为一步单次扫表,计算上更短平快。
-
必须已知环境模型 (Model-Based): 根据动态规划的思想,状态价值的推导依赖于状态转移函数(概率 P)和奖励函数(R)。如果你面对的是一个规则未知的"黑盒"环境,策略迭代就无法执行了(必须改用蒙特卡洛等无模型方法)。
-
面临"维度灾难": 每次评估和提升都需要遍历所有的状态空间和动作空间。一旦环境非常复杂(如围棋、自动驾驶),状态数呈指数级爆炸,传统的查表式策略迭代会因内存和算力瓶颈直接失效。
四、 适用场景
结合上述优缺点,策略迭代算法最适合以下场景:
-
环境规律完全已知(白盒环境): 能够明确写出状态转移概率和奖励机制的场景,例如走迷宫、特定规则的棋盘游戏,或参数完全确定的工业机械臂路径规划。
-
状态和动作空间较小且离散: 计算机内存足以用二维数组(查表法)存下所有的状态和动作组合。
-
对策略的精确度要求极高: 当任务规模可控,且你需要一个数学上 100% 完美的精确最优策略时,策略迭代是非常稳妥的选择。
价值迭代算法:
一、 价值迭代(已知模型)的执行流程

二、 价值迭代的优势

三、 价值迭代的劣势

四、 使用场景

代码架构:

algorithm.py ()
python
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import copy # 导入 copy 模块,用于策略矩阵的深拷贝操作
import numpy as np # 导入 numpy 模块,用于高效的矩阵与数组运算
class Algorithm: # 定义动态规划算法类,包含了策略迭代和价值迭代两种经典算法
def __init__(self, gamma, theta, episodes, state_size, action_size, logger):
self.state_size = state_size # 保存环境的状态空间总大小
self.action_size = action_size # 保存智能体的动作空间总大小
self.gamma = gamma # 保存折扣因子,用于衡量对未来奖励的重视程度
self.theta = theta # 保存价值收敛阈值,当价值变化小于该值时停止迭代
self.episodes = episodes # 保存算法允许的最大循环迭代次数
# 初始化智能体策略矩阵,每个状态下的每个动作概率均等(1/动作数),即均匀随机策略
self.agent_policy = np.ones([self.state_size, self.action_size]) / self.action_size
self.algo = "value_iteration" # 设定默认使用的动态规划算法类型为"价值迭代"
self.logger = logger # 保存日志记录器实例,用于输出训练进度
def learn(self, F): # 定义学习/求解入口函数,F 为环境的状态转移字典
assert self.algo in ["policy_iteration", "value_iteration"], "Invalid algorithm" # 确保指定的算法名称是合法的
if self.algo == "policy_iteration": # 如果选择的是"策略迭代"算法
self.policy_iteration(F) # 调用策略迭代主逻辑
elif self.algo == "value_iteration": # 如果选择的是"价值迭代"算法
self.value_iteration(F) # 调用价值迭代主逻辑
def policy_iteration(self, F): # 策略迭代算法实现
# 初始化一个所有动作概率相等的均匀随机策略矩阵
policy = np.ones([self.state_size, self.action_size]) / self.action_size
i = 0 # 初始化迭代轮次计数器
while i < self.episodes: # 在达到最大迭代次数前持续循环
V = self.policy_evaluation(policy, F) # 【策略评估】计算当前策略下各个状态的真实价值 V
Q = self.q_value_iteration(V, F) # 基于评估出的状态价值 V,计算出每个状态-动作对的动作价值 Q
new_policy = self.policy_improvement(Q) # 【策略提升】根据 Q 表,贪心地生成(更新)出一个全新的策略
# 检查新生成的策略与上一轮的策略是否完全一致(差异容忍度为 1e-3)
if np.allclose(policy, new_policy, atol=1e-3):
break # 如果策略不再发生改变,说明已经收敛到最优策略,跳出大循环
policy = copy.copy(new_policy) # 将提升后的新策略拷贝替换旧策略,准备进入下一轮迭代
if i % 10 == 0: # 每迭代 10 轮
self.logger.info("Iteration {}".format(i)) # 打印当前迭代轮次的日志信息
i += 1 # 迭代轮数加 1
self.agent_policy = policy # 将最终收敛的最优策略保存到类属性中
return policy, V # 返回求解得到的最优策略矩阵和最优状态价值矩阵
def value_iteration(self, F): # 价值迭代算法实现
V = np.zeros(self.state_size) # 初始化所有状态的价值为 0
i = 0 # 初始化迭代轮次计数器
while i < self.episodes: # 在达到最大迭代次数前持续循环
delta = 0 # 初始化本轮迭代中全状态价值的最大变化量为 0
for state in range(self.state_size): # 遍历环境中的每一个状态
v = V[state] # 暂存当前状态在更新前的旧价值
# 【贝尔曼最优方程核心】遍历所有动作计算期望回报,直接将"最大回报"作为该状态的新价值
V[state] = max(self._get_value(state, action, F, V) for action in range(self.action_size))
delta = max(delta, abs(v - V[state])) # 计算新旧价值的差值,并更新本轮的最大变化量 delta
if delta < self.theta: # 如果本轮所有状态的价值变化量都低于了设定的收敛阈值 theta
self.episodes_self = i # 记录下算法实际收敛所花费的迭代次数
break # 价值函数已收敛,跳出大循环
# 价值迭代中,每次更新价值后,隐式地进行一轮策略提升以备记录
policy = self.policy_improvement(self.q_value_iteration(V, F))
if i % 10 == 0: # 每迭代 10 轮
self.logger.info("Iteration {}".format(i)) # 打印当前迭代轮次的日志信息
i += 1 # 迭代轮数加 1
self.agent_policy = policy # 将提取出的最优策略保存到类属性中
return policy, V # 返回最优策略矩阵和最优状态价值矩阵
def policy_evaluation(self, policy, F): # 策略评估实现:计算给定策略下各状态的期望价值
V = np.zeros(self.state_size) # 初始化状态价值数组为 0
delta = self.theta + 1 # 赋予 delta 一个大于收敛阈值的初值,确保能够进入 while 循环
while delta > self.theta: # 只要价值的最大变化量仍然大于阈值,就继续评估更新
delta = 0 # 每一轮评估开始时,将最大变化量重置为 0
for state in range(self.state_size): # 遍历每一个状态
v = 0 # 用于累加当前状态下采取所有可能动作的期望回报
for action, action_prob in enumerate(policy[state]): # 遍历当前策略在该状态下给出的所有动作及其概率
# 累加公式:动作概率 * 采取该动作后的期望回报 (贝尔曼期望方程)
v += action_prob * self._get_value(state, action, F, V)
delta = max(delta, abs(v - V[state])) # 计算当前状态更新前后的价值差,更新整轮的 delta
V[state] = v # 将累加算出的新期望价值赋给该状态
return V # 价值完全收敛后,返回该策略对应的状态价值数组 V
def q_value_iteration(self, V, F): # 工具函数:利用状态价值 V 计算完整的动作价值 Q 表
Q = np.zeros([self.state_size, self.action_size]) # 初始化一个全 0 的 Q 表,维度为 [状态数, 动作数]
for state in range(self.state_size): # 遍历每一个状态
for action in range(self.action_size): # 遍历该状态下可执行的每一个动作
Q[state][action] = self._get_value(state, action, F, V) # 计算并在 Q 表中填入相应的动作价值
return Q # 返回计算完整的 Q 表
def policy_improvement(self, Q): # 策略提升实现:利用 Q 表生成更优的新策略
policy = np.zeros([self.state_size, self.action_size]) # 初始化一个全新的、纯空白的策略矩阵
for state in range(self.state_size): # 遍历每一个状态
action_values = Q[state] # 提取当前状态对应的所有动作的 Q 值集合
# 【贪心策略】找到 Q 值最大的那个动作的索引,利用 np.eye (独热编码) 将该最优动作的执行概率设为 1,其余为 0
policy[state] = np.eye(self.action_size)[np.argmax(action_values)]
return policy # 返回经过贪心提升后的确定性最优策略
def _get_value(self, state, action, F, V): # 辅助计算函数:获取在某状态下执行某动作的具体期望回报
value = 0 # 初始回报值设为 0
try: # 使用异常捕获处理查表过程,防止查询到非法的环境转移(如撞墙)
# 从环境转移字典 F 中查询当前(状态, 动作)对对应的:下一个状态、即时奖励 (忽略了 done 标志)
next_state, reward, _ = F[str(state)][str(action)]
if reward == 0: # 奖励塑形机制:如果环境原本给的奖励是 0
reward = -1 # 强制将其修改为 -1,作为"步数惩罚",促使智能体寻找最快到达终点的路径
value = reward + self.gamma * V[next_state] # 核心计算:即时奖励 + 衰减后的下一个状态价值
except KeyError: # 如果在转移字典中找不到这个 (状态, 动作) 键值对,说明是无效行为
pass # 忽略错误,保持 value 默认的 0 不变
return value # 返回计算好的动作期望回报值
train_workflow.py
python
import time # 导入 time 模块,用于计算动态规划算法收敛所消耗的时间
import os # 导入 os 模块,用于获取进程ID,以便区分多进程下的监控数据
from tools.map_data_utils import read_map_data # 导入地图读取工具,这是动态规划的核心(加载环境模型)
from tools.train_env_conf_validate import read_usr_conf # 导入配置读取工具,用于加载环境与算法相关的超参数
from tools.metrics_utils import get_training_metrics # 导入指标拉取工具,用于获取底层的系统训练指标
from common_python.utils.workflow_disaster_recovery import handle_disaster_recovery # 导入容灾模块(维持统一框架接口,虽然此离线算法较少触发)
def workflow(envs, agents, logger=None, monitor=None, *args, **kwargs): # 定义动态规划算法的主训练工作流函数
try: # 使用 try-except 块包裹核心逻辑,捕获并处理所有未预期的异常
usr_conf = read_usr_conf("agent_dynamic_programming/conf/train_env_conf.toml", logger) # 读取动态规划环境专用的 TOML 配置文件
if usr_conf is None: # 如果配置文件不存在或解析失败
logger.error("usr_conf is None, please check agent_dynamic_programming/conf/train_env_conf.toml") # 打印严重错误日志
return # 缺少配置,直接终止训练工作流
env, agent = envs[0], agents[0] # 取出实例列表中的第一个环境与智能体(适用于单智能体设定)
monitor_data = { # 初始化监控数据字典
"reward": 0, # 动态规划没有"打游戏收集奖励"的过程,这里仅为兼容监控平台接口而预留 0
}
logger.info("Start Training...") # 打印日志,宣布动态规划计算开始
start_time = time.time() # 记录求解算法开始的绝对时间戳
# 【重点】动态规划需要完全已知环境模型,因此必须直接加载包含了所有物理规则的地图数据文件
map_data_file = "conf/map_data/F_level_1.json" # 指定包含全地图状态转移函数(F字典)的 JSON 文件路径
map_data = read_map_data(map_data_file) # 读取并解析地图数据,构建状态动作对到 (next_state, reward, done) 的映射
if map_data is None: # 如果地图模型数据读取失败
logger.error(f"Failed to read map_data from file {map_data_file}, please check") # 报错提示
return # 没有环境底层模型,动态规划寸步难行,直接终止
# 【核心步骤】将全局地图模型 map_data 直接喂给智能体,智能体在内部执行价值迭代或策略迭代的疯狂扫表计算
agent.learn(map_data)
logger.info(f"Training time cost: {time.time() - start_time} s") # 算法收敛后,计算并打印出求得最优策略所花费的总耗时
monitor_data["reward"] = 0 # 离线求解结束,再次将监控汇报值赋为 0
if monitor: # 如果外部传来了有效的监控组件
monitor.put_data({os.getpid(): monitor_data}) # 将当前进程的监控状态推送到系统大盘
# 将动态规划算出的、能够 100% 完美通关的"上帝视角"策略保存到磁盘文件中
agent.save_model()
training_metrics = get_training_metrics() # 获取底层训练框架可能统计到的一些系统侧指标
if training_metrics: # 如果获取成功
logger.info(f"training_metrics is {training_metrics}") # 把详细指标打在日志里供开发者排查
except Exception as e: # 捕获以上全部流程中任何可能发生的报错
raise RuntimeError(f"workflow error: {e}") # 将原本的异常包装成带上下文的 RuntimeError 继续向上层抛出
agent.py
python
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import numpy as np # 导入 numpy 库,用于进行高效的矩阵与数组运算(如 argmax 操作)
from common_python.utils.common_func import create_cls # 导入辅助函数,用于动态创建简易的数据封装类
from kaiwudrl.interface.agent import BaseAgent # 从框架接口中导入智能体的基类 BaseAgent
from agent_dynamic_programming.conf.conf import Config # 导入配置文件,获取状态空间、动作空间、折扣因子等超参数
from agent_dynamic_programming.algorithm.algorithm import Algorithm # 导入我们上文详细剖析过的动态规划(价值迭代/策略迭代)算法实体
ObsData = create_cls("ObsData", feature=None) # 动态定义 ObsData 数据类,包含 feature 属性,用于传递给算法
ActData = create_cls("ActData", act=None) # 动态定义 ActData 数据类,包含 act 属性,用于传递给环境
class Agent(BaseAgent): # 定义动态规划智能体类,继承自框架提供的 BaseAgent
def __init__(self, agent_type="player", device=None, logger=None, monitor=None) -> None: # 初始化函数,接收设备、日志和监控实例
self.logger = logger # 保存日志记录器实例,用于在工作流中打印关键信息
# 实例化动态规划算法对象,严格按照 Algorithm 类的要求,从 Config 中注入所有必需的超参数
self.algorithm = Algorithm(
Config.GAMMA, Config.THETA, Config.EPISODES, Config.STATE_SIZE, Config.ACTION_SIZE, self.logger
)
super().__init__(agent_type, device, logger, monitor) # 调用父类的初始化方法,完成底层通信与调度机制的挂载
def predict(self, list_obs_data): # 定义在运行阶段预测动作的方法(批量接口)
state = int(list_obs_data[0].feature) # 从第一条(也是当前唯一一条)观测数据中提取出 1 维的状态索引值
# 【动态规划特性】无需 epsilon 探索!直接查表,找到该状态在训练好的最优策略矩阵中概率最大(通常是 1.0)的那个动作
action = int(np.argmax(self.algorithm.agent_policy[state]))
return [ActData(act=action)] # 将得到的整数动作指令封装成 ActData 对象,包装在列表中返回
def exploit(self, env_obs): # 定义利用方法,通常用于严格的纯评估/测试阶段
obs_data = self.observation_process(env_obs) # 对环境原始返回的庞大观测字典进行预处理,提取出精简特征
state = obs_data.feature # 拿到降维后的 1 维状态索引
# 同样直接利用收敛后的策略矩阵进行 100% 贪心选择,并打包为 ActData
act_data = ActData(act=int(np.argmax(self.algorithm.agent_policy[state])))
action = self.action_process(act_data) # 解包 ActData,转化为环境可以直接执行的原始数值指令
return action # 返回环境需要的动作
def learn(self, state_transition_function): # 定义学习函数
# 动态规划属于"有模型"学习,直接将完整包含地图物理规则的状态转移字典透传给底层算法,进行上帝视角的暴力扫表推导
self.algorithm.learn(state_transition_function)
def observation_process(self, env_obs): # 定义观测预处理函数
obs = env_obs["observation"] # 从大字典中剥离出实际的观测信息
pos = [obs["frame_state"]["hero"]["pos"]["x"], obs["frame_state"]["hero"]["pos"]["z"]] # 提取英雄(智能体)在迷宫中的 2D 坐标位置
# 降维处理:假设地图横向宽度为 64,将 (x, z) 的二维坐标映射为唯一的一维整数索引,便于数组查表
pos_feature = int(pos[0] * 64 + pos[1])
return ObsData(feature=pos_feature) # 将计算出的 1D 状态索引放入 ObsData 中返回
def action_process(self, act_data): # 定义动作处理函数
return act_data.act # 从预测输出的包装对象中,提取出纯粹的数值动作指令返回给环境执行
def save_model(self, path=None, id="1"): # 定义模型持久化保存函数
model_file_path = f"{path}/model.ckpt-{str(id)}.npy" # 拼接模型保存的完整路径,严格符合 model.ckpt-id 命名规范
np.save(model_file_path, self.algorithm.agent_policy) # 将动态规划算出的绝对最优策略矩阵直接序列化保存为 .npy 文件
self.logger.info(f"save model {model_file_path} successfully") # 在日志中打印模型保存成功的确认信息
def load_model(self, path=None, id="1"): # 定义模型加载函数,用于比赛或效果回放
model_file_path = f"{path}/model.ckpt-{str(id)}.npy" # 拼接需要读取的模型文件路径
try: # 使用 try-except 包裹文件读取过程,防止文件缺失导致系统崩溃
self.algorithm.agent_policy = np.load(model_file_path) # 从磁盘读取 .npy 文件,反序列化后直接覆盖掉当前的策略矩阵
self.logger.info(f"load model {model_file_path} successfully") # 日志打印加载成功
except FileNotFoundError: # 捕获文件未找到的异常
self.logger.info(f"file {model_file_path} not found") # 打印严重的错误日志
exit(1) # 如果无法加载模型,直接以错误状态码 1 强制终止程序的运行