5分钟入门强化学习之动态规划算法与实现

一、 策略迭代的执行流程

策略迭代的核心是一个"左脚踩右脚"螺旋上升的过程,通过不断循环交替进行"策略评估"和"策略提升",直至策略不再发生改变。

  1. 初始化: 设定一个初始策略(如上下左右概率各 25% 的均匀随机策略),并将所有状态的初始价值设为 0。

  2. 策略评估 (Policy Evaluation): 在给定当前策略的前提下,利用贝尔曼期望方程反复迭代,计算出该策略在所有状态下的真实期望回报(即状态价值函数)。在实际代码中,为了节省算力,当状态价值的最大变化差值小于设定的极小阈值(如 0.001)时,即可认为评估收敛并提前结束该步骤。

  3. 策略提升 (Policy Improvement): 根据"策略评估"阶段算出的最新状态价值,计算出在当前状态下采取每一个合法动作能带来的"动作价值"。然后,直接贪心地选择能带来最大动作价值的动作,作为新策略在该状态下的唯一选择。

  4. 收敛判断与循环: 检查经过提升后得到的新策略,是否与前一轮的旧策略完全一模一样。如果一模一样,说明策略已经到达巅峰无法再优化,算法停止并输出最优策略;如果不同,则用新策略替换旧策略,回到第 2 步继续"评估"。

二、 策略迭代的优势

  1. 宏观收敛极快(迭代轮数少): 虽然它的内部计算复杂,但从整体策略更新的次数来看,策略迭代通常只需要极少的大循环就能找到最优解。例如在悬崖漫步(Cliff Walking)环境中,智能体仅仅经历了 5 次"评估-提升"的交替循环,策略就完全收敛了。

  2. 理论严谨,必定最优: 基于策略提升定理,每一次更新产生的新策略必定优于(或等于)旧策略。只要在有限的 MDP 中,它能提供数学上严谨的绝对精确解。

三、 策略迭代的劣势

  1. 单次循环的计算代价极大: 在每一轮大循环中,单单是"策略评估"这一步就需要不断做贝尔曼期望方程迭代,直至价值收敛,这会耗费非常大的计算代价。相比之下,价值迭代(Value Iteration)直接把这两步融为一步单次扫表,计算上更短平快。

  2. 必须已知环境模型 (Model-Based): 根据动态规划的思想,状态价值的推导依赖于状态转移函数(概率 P)和奖励函数(R)。如果你面对的是一个规则未知的"黑盒"环境,策略迭代就无法执行了(必须改用蒙特卡洛等无模型方法)。

  3. 面临"维度灾难": 每次评估和提升都需要遍历所有的状态空间和动作空间。一旦环境非常复杂(如围棋、自动驾驶),状态数呈指数级爆炸,传统的查表式策略迭代会因内存和算力瓶颈直接失效。

四、 适用场景

结合上述优缺点,策略迭代算法最适合以下场景:

  1. 环境规律完全已知(白盒环境): 能够明确写出状态转移概率和奖励机制的场景,例如走迷宫、特定规则的棋盘游戏,或参数完全确定的工业机械臂路径规划。

  2. 状态和动作空间较小且离散: 计算机内存足以用二维数组(查表法)存下所有的状态和动作组合。

  3. 对策略的精确度要求极高: 当任务规模可控,且你需要一个数学上 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 强制终止程序的运行
相关推荐
老虾头1 小时前
AI工具在传统行业服务升级中的应用案例分享
人工智能
SNKXD_11 小时前
2026品牌运营团队AI营销培训:TOP5轻量化课程适配常态化技能升级学习
大数据·人工智能·学习
Nan-h11 小时前
AI 浏览器怎么选:侧边栏助手、浏览器 Agent 和可复用工作流的差别
人工智能·ai浏览器
TMT星球1 小时前
AI时代的风控攻防战:Soul如何用AI治理AI
大数据·人工智能
Agent手记1 小时前
电信运营商如何用AI实现携号转网自动处理?基于实在Agent的业务自动化落地与TARS大模型解析方案
运维·人工智能·ai·自动化
scx_link1 小时前
线性回归的总结:
算法·机器学习·线性回归
郝亚军1 小时前
IEEE 754 单精度浮点的SEM表示
开发语言·c++·算法
肖有米XTKF86462 小时前
肖有米开发团队:初语山言商城系统开发-初语山言模式制度解析
大数据·团队开发·csdn开发云
麦哲思科技任甲林2 小时前
全变更蒸馏:让AI编程成为一个可进化的系统
人工智能·ai编程·蒸馏·skills·harness工程·回顾