5分钟入门强化学习之蒙特卡洛(MC)算法与实现

蒙特卡洛(MC)算法:

一、 蒙特卡洛方法的执行流程

蒙特卡洛方法的核心思想非常朴素:利用大数定律,通过大量重复的随机抽样(实战模拟),用所有采样结果的平均回报来逼近一个状态的真实期望回报(即状态价值)。

它的具体执行流程如下:

  1. 生成完整序列(采样): 智能体依据当前的策略 \\pi,在环境中从初始状态出发,不断执行动作,直到遇到终止状态(或达到最大时间步),从而收集到一条完整的经验序列(Episode):

  2. 计算累积回报: 当一整条序列结束后,算法通常采用"从后往前"逆向推导的技巧,计算序列中每一个状态往后所能获得的总累积奖励(即回报 G)。

  3. 记录与更新:

    对于序列中出现的每一个状态 s,记录它的回报。在实际代码中,通常采用增量式更新公式来实时修正价值估计,以节省内存:

    其中,N(s) 是状态被访问的次数。当采样次数足够多时,估计的价值 V(s) 就会收敛于真实的价值 v_{\\pi}(s)

补充细节: 在更新时,分为首次访问法(First-Visit) (一条序列中只在状态第一次出现时计算回报)和每步访问法(Every-Visit)(只要状态出现就计算回报)两种统计方式。

二、 蒙特卡洛方法的优势

  1. 无需知晓环境模型(Model-Free): 这是它最大的优势。在许多实际问题中,我们根本无法写出环境的状态转移概率 P 和奖励函数 R。蒙特卡洛方法允许智能体直接通过在环境中"闭眼摸索"收集真实数据来评估价值,不需要环境的物理引擎或底层代码。

  2. 状态估计相互独立:

    在动态规划(如策略评估)中,更新一个状态的价值必须要依赖于它下一个状态的价值(Bootstrapping / 自举)。但蒙特卡洛方法是对每一个状态的回报进行独立采样和平均,计算一个状态的价值不需要依赖其他状态的估计值。

  3. 直观且无偏:

    只要采样数量足够大,它算出来的平均回报就是该状态价值的无偏估计(尤其是首次访问法),不容易受到初始错误估计值的干扰。

三、 蒙特卡洛方法的劣势

  1. 必须等待序列结束(回合更新): 因为蒙特卡洛方法需要计算真实的总回报 G,所以智能体必须等一把游戏彻底打完(遇到终止状态)才能回头去更新各个状态的价值。如果一个任务非常长,甚至是没有终点的"连续任务",蒙特卡洛方法就直接罢工了。

  2. 方差较大:

    由于一条序列是由无数个随机的状态转移和随机的动作组成的,每一次采样走到终点所获得的累计奖励可能天差地别。这种极高的随机性导致蒙特卡洛方法估计的"方差很大",需要海量的采样数据才能稳定收敛。

  3. 只适用于同策略(On-Policy)采样的基础版本: 基础的蒙特卡洛方法通常要求评估的数据必须是由"当前正在优化的策略"亲自跑出来的。这意味着过去收集的旧数据一旦策略更新就会作废,样本利用率相对较低。

四、 使用场景

结合它的优缺点,蒙特卡洛方法最适合以下场景:

  • 回合制或有明确终点的任务(Episodic Tasks): 例如下围棋、打扑克、玩超级玛丽、或者是我们代码中举例的"不超过一定步数"的迷宫游戏等,必须有明确的结局(输、赢、通关或死亡)。

  • "黑盒"环境: 当你面对一个极其复杂、无法建立精确数学模型(不知道 PR)的真实世界系统时,通过实战模拟来采样是唯一的出路。

  • 重点关注部分状态: 如果你只关心某几个特定起始状态的价值,而不是要求解整个庞大环境所有状态的价值,通过蒙特卡洛方法针对这几个状态进行针对性采样是非常高效的。

针对蒙特卡洛方法中必须要"等游戏结束才能更新"的这个痛点,后来强化学习发展出了时间差分算法(TD Learning),它可以走一步就更新一步。


代码实现:

架构:

algorithm.py

python 复制代码
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

import numpy as np  # 导入numpy库,用于矩阵计算和随机数生成

class Algorithm:  # 定义蒙特卡洛强化学习算法类
    def __init__(self, gamma, state_size, action_size):  # 初始化函数,传入折扣因子、状态空间大小和动作空间大小
        self.gamma = gamma  # 存储折扣因子,用于控制对长远未来奖励的重视程度
        self.state_size = state_size  # 存储环境中的总状态数量
        self.action_size = action_size  # 存储智能体可以采取的总动作数量

        self.policy = np.random.choice(self.action_size, self.state_size)  # 随机初始化策略,为每个状态随机指定一个动作
        self.Q = np.zeros([self.state_size, self.action_size])  # 初始化动作价值函数(Q表)为全0矩阵,大小为 [状态数, 动作数]
        self.visit = np.zeros([self.state_size, self.action_size])  # 初始化访问计数器为全0矩阵,记录每个(状态,动作)对被访问的总次数


    def learn(self, list_sample_data):  # 定义学习函数,接收一整条完整的历史采样序列(episode)
        G, state_action_return = 0, []  # 初始化累积回报G为0,并创建一个空列表用于临时存储(状态, 动作, 该步对应的回报)

        # 逆序遍历采样数据(剔除最后一个样本,因为到达终止状态时通常没有后续动作)
        for sample in reversed(list_sample_data[:-1]):  
            state_action_return.append((sample["state"], sample["action"], G))  # 将当前步的状态、动作,以及其**之后**的累积回报G存入列表
            G = self.gamma * G + sample["reward"]  # 利用逆向推导更新当前步的累积回报G:当前步奖励 + 衰减后的后续回报

        state_action_return.reverse()  # 由于刚才列表是逆序添加的,现在将其反转回正序(时间从头到尾的顺序)

        seen_state_action = set()  # 初始化一个空集合,用于追踪在当前序列中已经处理过的(状态, 动作)对
        for state, action, G in state_action_return:  # 正序遍历处理好的状态、动作和对应的回报数据
            if (state, action) not in seen_state_action:  # 核心:首次访问法(First-Visit),只处理一条序列中第一次出现的(状态, 动作)
                self.visit[state][action] += 1  # 将该(状态, 动作)对的历史总访问次数加1
                
                # 增量式更新公式:Q(s,a) = Q(s,a) + (G - Q(s,a)) / N(s,a)
                self.Q[state, action] = self.Q[state, action] + (G - self.Q[state, action]) / self.visit[state, action]  
                
                seen_state_action.add((state, action))  # 将处理过的(状态,动作)对加入集合,确保该序列中后续同类对被忽略

        for state in range(self.state_size):  # 遍历整个状态空间,开始执行策略提升(Policy Improvement)
            best_action = np.argmax(self.Q[state])  # 运用贪心法,找出当前状态下对应Q值最大(期望回报最高)的那个动作
            self.policy[state] = best_action  # 将该状态下的最优动作更新到当前策略中

        return  # 单次序列的学习与策略更新完成,返回

conf.py

python 复制代码
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

class Config:  # 定义配置类,用于集中管理强化学习算法的环境维度与训练超参数
    
    STATE_SIZE = 64 * 64     # 状态空间的总大小(例如一个 64x64 的网格地图环境,共计 4096 个离散状态)
    ACTION_SIZE = 4          # 动作空间的总大小(例如智能体可以执行上、下、左、右 4 种不同的动作)
    LEARNING_RATE = 0.9      # 学习率 (Alpha),控制在价值更新时,新获取的经验覆盖旧经验的比例(0.9表示非常看重新经验)
    GAMMA = 0.9              # 折扣因子 (Gamma),决定智能体对未来长远奖励的重视程度(0.9代表智能体比较有远见)
    EPSILON = 0.1            # 探索率 (Epsilon),用于 ε-贪心(ε-greedy)策略,意味着智能体有 10% 的概率随机采取动作以探索未知环境
    EPISODES = 1000          # 训练的总回合数(即智能体在环境中从头到尾完整运行/打游戏的局数)
    
    OBSERVATION_SHAPE = 250  # 观测特征的维度大小(例如智能体接收到的局部视野或状态,被编码压缩为一个长度为 250 的一维特征向量)

monitor_builder.py

python 复制代码
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

# 从 kaiwudrl 框架的通用监控模块中导入 MonitorConfigBuilder,用于通过代码构建监控大盘的配置
from kaiwudrl.common.monitor.monitor_config_builder import MonitorConfigBuilder

def build_monitor():
    monitor = MonitorConfigBuilder()  # 实例化监控配置构建器对象

    # 使用链式调用的方式,自顶向下构建整个监控面板的配置字典
    config_dict = (
        monitor.title("峡谷漫步V2")  # 设置监控大盘的全局主标题为"峡谷漫步V2"
        .add_group(  # 添加一个新的指标大组(用于在页面上将相关图表归类展示)
            group_name="算法指标",  # 该指标分组的中文显示名称
            group_name_en="algorithm",  # 该指标分组的英文唯一标识(常用于后端路由或ID)
        )
        .add_panel(  # 在"算法指标"这个分组下,添加一个新的具体监控图表(面板)
            name="累积回报",  # 图表的中文显示名称
            name_en="reward",  # 图表的英文唯一标识
            type="line",  # 设置图表类型为折线图(line chart),适合展示回报随时间/回合的趋势
            unit="",  # 设置 Y 轴的数据单位(由于回报是一个纯数值,所以这里留空)
        )
        .add_metric(  # 往当前图表中注入实际的数据查询/计算逻辑
            metrics_name="reward",  # 绑定的后端遥测数据指标名称(即智能体上报的 reward 数据)
            expr="avg(reward{})",  # 查询表达式(类似于 PromQL),此处表示计算该指标的平均值
        )
        .end_panel()  # 结束当前"累积回报"图表(面板)的配置
        .end_group()  # 结束当前"算法指标"大组的配置
        .build()  # 触发构建动作,将上述所有的链式配置打包编译成一个标准的 Python 字典 (dict)
    )
    
    return config_dict  # 返回构建好的配置字典,交由底层系统去渲染真实的监控网页

definition.py

python 复制代码
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

def sample_process(list_game_data):  # 定义样本处理函数,将环境返回的原始游戏帧数据转化为算法可用的训练样本
    # 使用列表推导式,遍历所有游戏帧,提取关键信息(状态、动作、奖励)并打包为标准字典格式后返回
    return [{"state": frame.state, "action": frame.action, "reward": frame.reward} for frame in list_game_data]


def reward_shaping(env_reward, env_obs):  # 定义奖励塑形函数,通过人工干预调整奖励信号来引导/加速智能体学习(常用于蒙特卡洛等算法)
    score = env_obs["observation"]["env_info"]["score"]  # 从环境复杂的观测信息(dict)中,解析并提取出当前的环境基础得分
    terminated = env_obs["terminated"]  # 获取当前回合是否已经结束的标志位(True/False)

    reward = 0  # 初始化经过塑形处理后的最终奖励值为 0

    reward += score  # 将环境给定的基础得分直接计入总奖励中

    if not terminated:  # 核心逻辑:如果当前回合尚未结束(即智能体还在继续游玩耗时)
        reward += -1    # 给予 -1 的步数惩罚。这是一种经典技巧,能迫使智能体寻找最短/最快的路径通关,防止其在原地无效徘徊

    return reward  # 返回最终计算好的塑形奖励,交由后续算法(如累积回报计算)使用

train_workflow.py (完整的训练工作流与日志打印工作)

python 复制代码
import time  # 导入time模块,用于记录时间间隔和控制指标上报/模型保存的频率
import math  # 导入math模块,用于计算基于指数衰减的自适应探索率(epsilon)
import os    # 导入os模块,用于获取当前进程ID以区分多进程的监控数据
from common_python.utils.common_func import Frame  # 导入Frame数据结构,用于封装单步的(状态, 动作, 奖励)数据
from agent_monte_carlo.feature.definition import sample_process, reward_shaping  # 导入上文定义的样本格式化和奖励塑形函数
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_monte_carlo/conf/train_env_conf.toml", logger)  # 读取并解析指定的 TOML 格式环境配置文件
        if usr_conf is None:  # 如果配置文件加载失败或不存在
            logger.error("usr_conf is None, please check agent_monte_carlo/conf/train_env_conf.toml")  # 记录错误日志,提示检查配置文件路径
            return  # 配置文件异常,直接退出工作流,终止训练

        env, agent = envs[0], agents[0]  # 从列表中取出第一个环境实例和第一个智能体实例(适用于单智能体单环境设定)
        EPISODES = 1000  # 设置最大训练回合数限制为 1000 局

        monitor_data = {  # 初始化一个字典,用于暂存需要上报到外部监控大盘的指标数据
            "reward": 0,  # 将初始平均奖励预设为 0
        }
        last_report_monitor_time = 0  # 初始化上一次向监控系统上报数据的时间戳为 0
        last_get_training_metrics_time = 0  # 初始化上一次拉取底层训练指标的时间戳为 0

        logger.info("Start Training...")  # 打印日志,标记强化学习训练正式开始
        start_time = time.time()  # 记录整个训练流程开始时的绝对时间戳,用于最终计算总耗时
        last_save_model_time = start_time  # 将上一次保存模型的时间初始化为训练开始时刻

        total_reward = 0  # 初始化累计奖励总和,用于计算每个监控汇报周期内的平均奖励
        episode_count = 0  # 初始化已完成的回合计数器(在每个监控汇报周期结束后会清零重置)
        win_count = 0  # 初始化智能体成功通关/获胜的总次数,用于后续计算胜率

        for episode in range(EPISODES):  # 开始进入按回合(episode)进行的训练大循环
            if time.time() - last_get_training_metrics_time > 15:  # 检查当前时间距离上次拉取底层指标是否超过了 15 秒
                last_get_training_metrics_time = time.time()  # 更新最近一次拉取底层指标的时间戳为当前时间
                training_metrics = get_training_metrics()  # 调用外部接口获取当前的训练框架系统级指标
                if training_metrics:  # 如果成功获取到了指标数据
                    logger.info(f"training_metrics is {training_metrics}")  # 将系统指标打印到日志中供开发者调试参考

            env_obs = env.reset(usr_conf=usr_conf)  # 调用环境的 reset 方法重置环境到初始状态,并传入用户配置,获取初始观测数据

            if handle_disaster_recovery(env_obs, logger):  # 检查重置后的环境观测是否存在异常(例如底层服务挂掉),执行容灾处理
                continue  # 如果触发了容灾机制,跳过当前回合的后续逻辑,直接重新尝试开始下一个回合

            # 核心机制:根据当前的历史总胜率,利用指数衰减函数动态计算并更新智能体的探索率 epsilon,强制保底不低于 0.1
            agent.epsilon = max(0.1, math.exp(-0.5 / (1 - win_count / (episode + 1))))

            done = False  # 初始化当前单局游戏的结束标志位为 False
            episode_trajectory = []  # 初始化一个空列表,用于完整记录当前这一局游戏从头到尾的所有帧数据(即轨迹)

            while not done:  # 进入单局游戏内部的交互循环,直到游戏宣告结束才跳出
                obs_data = agent.observation_process(env_obs)  # 智能体对环境返回的原始观测数据进行特征提取和格式预处理

                act_data = agent.predict(list_obs_data=[obs_data])  # 智能体基于当前观测和 ε-贪心策略,预测并生成下一步的动作数据包
                act_data = act_data[0]  # 从批量预测结果(list)中取出第一个(也是当前唯一一个)动作数据

                current_action = agent.action_process(act_data)  # 将算法内部的动作格式翻译转换为环境可识别、可执行的真实动作指令

                env_reward, env_obs = env.step(current_action)  # 将动作下发给环境执行,环境物理引擎推演一步后返回即时奖励和下一步的新观测

                if handle_disaster_recovery(env_obs, logger):  # 交互一步后再次检查环境是否发生异常中断
                    break  # 如果触发容灾异常,直接强行中断并放弃当前这局游戏

                terminated, truncated = env_obs["terminated"], env_obs["truncated"]  # 解析环境返回的完成状态:terminated(正常通关/死亡) 和 truncated(超时截断)

                reward = reward_shaping(env_reward, env_obs)  # 调用自定义的奖励塑形函数,加入步数惩罚等人工引导机制,重新计算该步奖励

                done = terminated or truncated  # 只要是正常结束或者超时截断,都认为当前这局游戏已经 done (完成)
                if terminated:  # 如果是正常达成游戏结局(比如成功走到迷宫终点)
                    win_count += 1  # 将总获胜/成功通关的次数加 1
                    current_action = None  # 游戏已经结束,最后一帧不再需要记录动作,强制置为空

                frame = Frame(state=obs_data.feature, action=current_action, reward=reward)  # 将当前的预处理状态、动作和塑形后的奖励封装成一个标准的 Frame 结构
                episode_trajectory.append(frame)  # 将这个封装好的数据帧追加到当前局的轨迹列表中保存
                total_reward += reward  # 将该步获得的奖励累加到当前监控周期的总奖励中

            episode_trajectory = sample_process(episode_trajectory)  # 游戏结束,调用样本处理函数将完整的轨迹帧列表转化为字典格式的训练样本

            agent.learn(episode_trajectory)  # 【蒙特卡洛核心】将一整局的完整轨迹送入智能体的 learn 函数,利用首次访问法逆推计算回报并更新 Q 表

            episode_count += 1  # 将当前监控周期内的已完成回合数加 1
            now = time.time()  # 获取当前时刻的绝对时间戳

            is_converged = win_count / (episode + 1) > 0.9 and episode > 200  # 判定收敛规则:如果总训练回合数超过 200 局,且历史总胜率超过 90%,则认为模型已收敛

            if now - last_report_monitor_time > 15 or is_converged:  # 检查距离上次上报监控数据是否超过了 15 秒,或者模型已经触发了收敛条件
                avg_reward = total_reward / episode_count  # 计算这个监控周期内所有游戏对局的平均回报
                logger.info(f"Episode: {episode + 1}, Avg Reward: {avg_reward}")  # 打印当前进行到的回合数以及近期的平均回报日志
                logger.info(f"Training Win Rate: {win_count / (episode + 1)}")  # 打印当前智能体的全局历史平均胜率
                monitor_data["reward"] = avg_reward  # 将计算出的平均回报写入待上报的监控字典中
                if monitor:  # 如果外部传入了有效的监控组件实例
                    monitor.put_data({os.getpid(): monitor_data})  # 以当前进程 ID 为 key,将监控数据推送至大盘消息队列

                total_reward = 0  # 监控周期结束,清空累计奖励总和,为下一个 15 秒的统计做准备
                episode_count = 0  # 清空监控周期内的回合计数器
                last_report_monitor_time = now  # 更新上一次监控上报的时间戳为当前时间

            if is_converged:  # 再次检查是否达到了前面设定的收敛标准(胜率 > 90%)
                logger.info(f"Training Converged at Episode: {episode + 1}")  # 打印日志,宣布训练已达到理想状态并成功收敛
                break  # 直接跳出最外层的 EPISODES 大循环,提前结束整个训练工作流

            if now - last_save_model_time > 300:  # 检查距离上次将模型保存到磁盘是否已经超过了 300 秒(5 分钟定时存档)
                logger.info(f"Saving Model at Episode: {episode + 1}")  # 打印日志,提示正在进行定时模型保存(Checkpoint)
                agent.save_model()  # 调用智能体的模型保存接口,将当前训练出的最优 Q 表和策略落盘
                last_save_model_time = now  # 更新上一次模型保存的时间戳为当前时间

        end_time = time.time()  # 训练大循环结束(不管是自然跑完还是收敛提前结束),记录最终结束时间
        logger.info(f"Training Time for {episode + 1} episodes: {end_time - start_time} s")  # 计算并打印整个训练过程所消耗的总时长(秒)
        agent.episodes = episode + 1  # 把实际训练跑完的总回合数记录到智能体对象的属性中备用

        agent.save_model()  # 训练彻底结束,执行最后一次模型保存,确保最新的收敛策略被持久化下来

    except Exception as e:  # 捕获整个工作流中可能发生的任何未预期的异常错误
        raise RuntimeError(f"workflow error: {e}")  # 将异常包装为 RuntimeError 并向上抛出,附带具体的错误信息以便溯源排查

agent.py

python 复制代码
import numpy as np  # 导入 numpy 库,用于高效的矩阵运算和随机数生成
from kaiwudrl.interface.agent import BaseAgent  # 从框架中导入智能体基类 BaseAgent
from common_python.utils.common_func import create_cls  # 导入用于动态创建数据结构类的辅助函数
from agent_monte_carlo.conf.conf import Config  # 导入包含了环境维度与超参数的配置类
from agent_monte_carlo.algorithm.algorithm import Algorithm  # 导入上文定义好的蒙特卡洛核心算法类

ObsData = create_cls("ObsData", feature=None)  # 动态创建一个名为 ObsData 的类,用于封装处理后的状态特征
ActData = create_cls("ActData", act=None)  # 动态创建一个名为 ActData 的类,用于封装智能体输出的动作数据

class Agent(BaseAgent):  # 定义蒙特卡洛智能体类,继承自 BaseAgent 基类
    def __init__(self, agent_type="player", device=None, logger=None, monitor=None) -> None:  # 初始化方法,接收智能体类型、设备、日志和监控实例
        self.logger = logger  # 保存日志记录器实例,以便在类内部各处打印运行日志

        self.state_size = Config.STATE_SIZE  # 从配置类中读取状态空间总大小(如 64x64=4096个状态)
        self.action_size = Config.ACTION_SIZE  # 从配置类中读取动作空间总大小(如上下左右 4 个方向)
        self.epsilon = Config.EPSILON  # 从配置类中读取初始探索率 Epsilon
        self.algorithm = Algorithm(Config.GAMMA, self.state_size, self.action_size)  # 实例化蒙特卡洛算法对象,传入折扣因子和空间大小

        super().__init__(agent_type, device, logger, monitor)  # 调用父类 BaseAgent 的初始化方法,完成底层基础配置

    def predict(self, list_obs_data):  # 定义预测方法,用于在训练阶段根据当前观测生成下一步动作
        state = list_obs_data[0].feature  # 从批量观测数据中取出第一条(当前步)的状态特征索引
        action = self._epsilon_greedy(state=state, epsilon=self.epsilon)  # 调用 ε-贪心策略函数,以一定概率随机探索或利用现有 Q 表给出动作

        return [ActData(act=int(action))]  # 将计算出的动作整数包装成 ActData 对象,并以列表形式返回给训练工作流

    def exploit(self, env_obs):  # 定义利用方法,通常用于模型评估或正式比赛阶段(完全不包含随机探索机制)
        obs_data = self.observation_process(env_obs)  # 对环境传来的原始观测数据进行预处理和特征提取
        state = obs_data.feature  # 取出处理好的 1D 状态索引特征
        act_data = ActData(act=int(self.algorithm.policy[state]))  # 直接查表,100% 贪心地取当前策略中该状态对应的最优动作,并包装成 ActData
        action = self.action_process(act_data)  # 将 ActData 解包为环境可直接执行的具体动作数值
        return action  # 返回要执行的绝对最优动作

    def _epsilon_greedy(self, state, epsilon=0.1):  # 定义 ε-贪心算法的具体实现,用于在训练时平衡探索(Exploration)与利用(Exploitation)
        if np.random.rand() < epsilon:  # 生成一个 0~1 的随机浮点数,如果小于设定的探索率 epsilon
            return int(np.random.randint(self.action_size))  # 则触发随机探索:在动作空间内等概率随机挑选一个动作并返回
        else:  # 否则(绝大多数情况下,概率为 1-epsilon)
            return int(self.algorithm.policy[state])  # 触发利用:直接从训练好的策略数组(Policy)中取出当前状态对应的最佳动作返回

    def learn(self, list_sample_data):  # 定义学习函数,接收一整局游戏的完整轨迹数据
        return self.algorithm.learn(list_sample_data)  # 直接将样本数据透传给底层蒙特卡洛算法对象的 learn 方法,进行 Q 表和策略的更新计算

    def observation_process(self, env_obs):  # 定义观测预处理函数,负责将环境复杂的字典状态映射为算法可用的简单特征
        obs = env_obs["observation"]  # 从环境观测大字典中提取核心的 observation 数据段
        pos = [obs["frame_state"]["hero"]["pos"]["x"], obs["frame_state"]["hero"]["pos"]["z"]]  # 提取英雄(智能体)在 2D 游戏地图上的具体坐标 (x, z)

        pos_feature = int(pos[0] * 64 + pos[1])  # 降维操作:将 2D 坐标 (x, z) 编码为一个一维数组索引(此处假设地图一排宽度为 64)

        return ObsData(feature=pos_feature)  # 将计算得到的一维状态特征封装成 ObsData 对象后返回

    def action_process(self, act_data):  # 定义动作处理函数,完成算法输出到环境输入的转换
        return act_data.act  # 从算法预测生成的 ActData 包装类中,剥离出纯粹的数值动作指令返回给环境

    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.policy)  # 使用 numpy 的 save 方法,将当前收敛的策略数组(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:  # 开启异常捕获,防止因为找不到模型文件而导致程序崩溃
            self.algorithm.policy = np.load(model_file_path)  # 使用 numpy 的 load 方法从 .npy 文件中反序列化加载策略数组,并覆盖当前的空白策略
            self.logger.info(f"load model {model_file_path} successfully")  # 打印加载成功的日志信息
        except FileNotFoundError:  # 如果捕获到文件不存在异常 (FileNotFoundError)
            self.logger.info(f"File {model_file_path} not found")  # 打印未找到文件的严重错误日志
            exit(1)  # 遇到找不到模型的致命错误,直接以异常状态码(1)强行终止程序进程

agent.py是干嘛的,与其他文件有什么关系

1. 与环境 (Environment) 的关系:它是"翻译官"

底层的数学算法是看不懂复杂的"游戏画面"或"环境字典"的,它只认识简单的数字索引。

输入翻译: agent.py 中的 observation_process 负责充当眼睛,把环境传来的复杂 2D 坐标 (x, z) 和游戏状态,降维压扁成一个简单的一维数字(特征索引),然后才交给大脑去思考 。
*

输出翻译: agent.py 中的 action_process 负责充当手脚,把算法决定好的动作数据,解包并转换成游戏环境能直接接收和执行的按键指令 。

2. 与 algorithm.py (底层算法) 的关系:它是"代理人与保护者"

包裹与隔离: agent.py 在初始化时实例化了 Algorithm 对象(即蒙特卡洛算法的核心) 。
*

决策包装: 纯粹的 Algorithm 只维护 Q 表(即一张价值打分表)。而 agent.py 中的 predict_epsilon_greedy 方法利用这张表,加入了 \\epsilon-贪心机制(以一定概率随机乱走),从而在"探索新路线"和"利用旧经验"之间做出平衡 。
*

转交学习任务: 当一局游戏打完,整理好整局的经验轨迹后,agent.py 会调用 learn 方法,但它自己不算数学题,而是直接把数据透传给 self.algorithm.learn() 让底层算法去更新矩阵 。

3. 与 workflow.py (训练工作流) 的关系:它是"被调度的打工人"

  • workflow.py 是整个训练场的"导演"或"裁判",它控制着游戏的开始、结束、何时重置环境、何时保存进度。

  • workflow 的主循环里,导演会在恰当的时机不断呼叫 Agent:

    • "现在环境变成这样了,Agent 你看看(observation_process),你下一步打算怎么走(predict)?"

    • "这局游戏结束了,这是所有的历史轨迹,Agent 你赶紧学习一下(learn)!"

    • "已经过去 5 分钟了,Agent 你把现在的脑子存个档(save_model)。"

4. 与 conf.py (配置文件) 的关系:它是"图纸读取者"

  • agent.py 诞生(初始化)时,它必须去读取 Config 里的图纸 。比如地图到底有多大(决定了状态空间 state_size)、智能体有几个技能(动作空间 action_size),以及探索率 \\epsilon 是多少,以此来正确地构建自己和底层的算法矩阵 。
相关推荐
weelinking1 小时前
【产品】10_搭建前端框架——把你的原型变成真实页面
java·大数据·前端·数据库·人工智能·python·前端框架
2601_958352901 小时前
AP-0316语音处理模组:适配音频设备的技术优势分析
人工智能·语音识别·硬件开发·回音消除·音频处理模块
dualven_in_csdn1 小时前
cmd切换到powershell (一)
服务器·开发语言·php
蜜蜜不吃糖1 小时前
解决Veeam备份数据到Backup copy服务器报错session log违反了检查约束
运维·服务器
x_xbx1 小时前
LeetCode:581. 最短无序连续子数组
算法·leetcode·排序算法
阿达_优阅达1 小时前
Sumsub 亚太巡展深圳站收官丨AI 信任自动化赋能出海企业合规破局
人工智能·ai·出海·合规·kyc·sumsub
星纬智联技术1 小时前
AI搜索引擎引用率提升指南:内容监控框架与引用偏好识别
人工智能·aigc·geo
不会计算机的g_c__b1 小时前
Argoverse API 完全解析:自动驾驶数据集与高精地图开发利器
人工智能·机器学习·自动驾驶
WangN21 小时前
【通识】具身智能、机器人、智能驾驶研发主线:世界模型与VLA技术深度调研
人工智能·机器人·自动驾驶·具身智能