蒙特卡洛(MC)算法:
一、 蒙特卡洛方法的执行流程
蒙特卡洛方法的核心思想非常朴素:利用大数定律,通过大量重复的随机抽样(实战模拟),用所有采样结果的平均回报来逼近一个状态的真实期望回报(即状态价值)。
它的具体执行流程如下:
-
生成完整序列(采样): 智能体依据当前的策略 \\pi,在环境中从初始状态出发,不断执行动作,直到遇到终止状态(或达到最大时间步),从而收集到一条完整的经验序列(Episode):
。
-
计算累积回报: 当一整条序列结束后,算法通常采用"从后往前"逆向推导的技巧,计算序列中每一个状态往后所能获得的总累积奖励(即回报 G)。
-
记录与更新:
对于序列中出现的每一个状态 s,记录它的回报。在实际代码中,通常采用增量式更新公式来实时修正价值估计,以节省内存:
其中,N(s) 是状态被访问的次数。当采样次数足够多时,估计的价值 V(s) 就会收敛于真实的价值 v_{\\pi}(s)。
补充细节: 在更新时,分为首次访问法(First-Visit) (一条序列中只在状态第一次出现时计算回报)和每步访问法(Every-Visit)(只要状态出现就计算回报)两种统计方式。
二、 蒙特卡洛方法的优势
-
无需知晓环境模型(Model-Free): 这是它最大的优势。在许多实际问题中,我们根本无法写出环境的状态转移概率 P 和奖励函数 R。蒙特卡洛方法允许智能体直接通过在环境中"闭眼摸索"收集真实数据来评估价值,不需要环境的物理引擎或底层代码。
-
状态估计相互独立:
在动态规划(如策略评估)中,更新一个状态的价值必须要依赖于它下一个状态的价值(Bootstrapping / 自举)。但蒙特卡洛方法是对每一个状态的回报进行独立采样和平均,计算一个状态的价值不需要依赖其他状态的估计值。
-
直观且无偏:
只要采样数量足够大,它算出来的平均回报就是该状态价值的无偏估计(尤其是首次访问法),不容易受到初始错误估计值的干扰。
三、 蒙特卡洛方法的劣势
-
必须等待序列结束(回合更新): 因为蒙特卡洛方法需要计算真实的总回报 G,所以智能体必须等一把游戏彻底打完(遇到终止状态)才能回头去更新各个状态的价值。如果一个任务非常长,甚至是没有终点的"连续任务",蒙特卡洛方法就直接罢工了。
-
方差较大:
由于一条序列是由无数个随机的状态转移和随机的动作组成的,每一次采样走到终点所获得的累计奖励可能天差地别。这种极高的随机性导致蒙特卡洛方法估计的"方差很大",需要海量的采样数据才能稳定收敛。
-
只适用于同策略(On-Policy)采样的基础版本: 基础的蒙特卡洛方法通常要求评估的数据必须是由"当前正在优化的策略"亲自跑出来的。这意味着过去收集的旧数据一旦策略更新就会作废,样本利用率相对较低。
四、 使用场景
结合它的优缺点,蒙特卡洛方法最适合以下场景:
-
回合制或有明确终点的任务(Episodic Tasks): 例如下围棋、打扑克、玩超级玛丽、或者是我们代码中举例的"不超过一定步数"的迷宫游戏等,必须有明确的结局(输、赢、通关或死亡)。
-
"黑盒"环境: 当你面对一个极其复杂、无法建立精确数学模型(不知道 P 和 R)的真实世界系统时,通过实战模拟来采样是唯一的出路。
-
重点关注部分状态: 如果你只关心某几个特定起始状态的价值,而不是要求解整个庞大环境所有状态的价值,通过蒙特卡洛方法针对这几个状态进行针对性采样是非常高效的。
针对蒙特卡洛方法中必须要"等游戏结束才能更新"的这个痛点,后来强化学习发展出了时间差分算法(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 是多少,以此来正确地构建自己和底层的算法矩阵 。