news 2026/11/2 6:55:01

从窗口截图到Gym封装:赛尔号Python登录器与强化学习实验台拆解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从窗口截图到Gym封装:赛尔号Python登录器与强化学习实验台拆解

简介:围绕赛尔号登录器开发与强化学习实践整理的压缩包,面向具备Python基础、希望了解游戏辅助工具与游戏AI实现的学习者和开发者。内容聚焦登录验证、HTTP请求、服务器数据交互等登录器关键环节,并延伸到Q-learning、DQN、PPO等算法在游戏角色决策中的尝试,帮助读者建立从环境搭建、状态与动作定义到策略训练的完整认知。压缩包共2000个文件、约42.75MB,以1258个txt说明与607个bmp图像素材为主,另含24个py脚本、dll/exe运行组件、dat配置及md笔记等,目录按模块分布,便于检索与二次开发。已有167人学习下载。读者可从中获得登录器网络通信与调试思路、强化学习奖励函数与探索利用平衡的设计参考,以及配套脚本、素材和运行组件。附带的文本说明与位图素材可辅助理解任务流程与界面状态,脚本则用于串联数据抓取、登录流程和训练实验,适合课程设计、项目练手及游戏AI入门实践。

1. 赛尔号与python:一个压缩包里的登录器与强化学习实验台

上周有人把一个压缩包丢过来,标题是《赛尔号与python:登陆器,强化学习等.zip》——「登陆」两个字还写错了,我一开始以为里面是几段零散的自动点击脚本。解压看完目录,结构比预想的规整得多:一个用窗口句柄抓图、模板匹配定位、状态机驱动的桌面登录器,一个把回合制对战抽象成 Gym 接口的强化学习环境,再加上训练和评估两个入口。它适合两种人:想练 Python 桌面自动化(窗口截图、图像定位、事件模拟)的新手,能照着把整条链路跑通;以及想找一个带真实 UI、非线性棋盘、动作空间又不大的强化学习练手场景的熟手,环境封装那部分留了足够多的改造口子。这篇就按「这包是什么 → 怎么跑起来 → 怎么改成自己的东西」拆一遍。

2. 项目结构与技术选型:包里的三层职责划分

拿到一个来路不明的压缩包,我习惯先不急着装依赖,先tree一遍看它想干什么。这个包的分层很清楚:底层是「看见屏幕、点得动」的能力层,中间是「登录、切号、进对战」的业务流程层,上层才是「把流程变成 MDP」的算法层。三层之间靠一个很薄的 driver 接口串起来,所以你可以只跑登录器、不碰强化学习,也可以只换算法、不动 UI 那一套。

2.1 目录树与模块职责

解压后的结构和职责大致如下(包内命名以实际为准,这里按功能归类):

路径职责改动频率
conf/yaml 配置、模板图、账号池示例高,每次换分辨率都要动
core/capture.py查找窗口、客户区截图、客户区坐标转屏幕坐标低
core/vision.py模板匹配、状态分类、画面稳定判定中
core/actions.py鼠标键盘事件、随机延迟、焦点切换低
login/登录流程、选服、切号、异常重连中
rl/env.pyGym 环境封装,step / reset 的实现高
rl/features.py画面到观测向量的解析高
rl/reward.py奖励函数,独立成一个文件方便调参高
train.py/eval.py训练与评估入口低
tools/模板图裁剪、坐标标定、单步调试小脚本低
build.spec打包配置低

这个划分有一个明显的好处:tools/里那几个小脚本是给人用的,不是给程序用的。标定坐标、裁剪模板图这类活儿如果每次都要改代码再跑一遍主流程,调试成本会高到离谱。我见过不少同类项目把标定逻辑硬塞进主循环,结果换台机器就是一场灾难。

2.2 技术选型:为什么是图像识别而不是 DOM 或内存

这类回合制网页游戏的客户端形态比较特殊:虽然内核是网页渲染,但页面跑在独立客户端里,外部拿不到稳定可用的 DOM 结构,等页面加载完再 querySelector 的常规路子走不通。所以这个包选了最土但最稳的方案——截图 + 模板匹配 + 模拟点击。

技术上用的是pywin32拿窗口句柄和客户区尺寸,opencv-python做模板匹配,pyautogui发鼠标事件,gymnasium定义环境接口,stable-baselines3跑 PPO。选 gymnasium 而不是自己造一套接口,主要是为了直接吃上 SB3 的向量化环境、VecNormalize和回调体系,省掉大量脚手架代码。日志用loguru,配置用PyYAML,打包用PyInstaller。

这里有个取舍值得说清楚:为什么不读内存?读内存确实快、准、无延迟,但游戏一更新,偏移量全变,维护成本直接爆炸,而且换台机器、换个版本就废。图像识别的血泪经验是「慢但不崩」——一帧截图 20 毫秒,模板匹配 5 毫秒,一回合几秒钟,这点延迟完全吞得下。强化学习那边真正在意的是采样吞吐,而这个项目在设计上把「决策频率」和「画面刷新频率」解耦了:一回合只出一帧观测,所以图像方案带来的延迟对训练影响很小。

3. 登录器模块拆解:从窗口绑定到状态机主循环

登录器这部分是整个包的地基。强化学习环境并不直接操作游戏,它调用的是登录器那套 driver:截一张图、点一个坐标、等画面稳定。所以这层写稳了,上层才有意义。

3.1 配置与账号池:先别把密码写进 yaml

配置文件是整条链路的入口,也是最容易被随手写烂的地方。包里给的示例把窗口标题、匹配阈值、单步超时都提出来了,用 dataclass 承接,避免满代码dict.get("xxx", 默认值)。

from dataclasses import dataclass, field from pathlib import Path import yaml @dataclass class Account: name: str password: str # 示例文件里是占位符,真实密码建议走环境变量或系统凭据库 server: str = "default" @dataclass class AppConfig: window_title: str = "回合制对战" # 窗口标题关键字,换客户端版本时要改 match_threshold: float = 0.86 # 模板匹配阈值,低于它宁可不点 action_timeout: float = 8.0 # 单个状态最长等待秒数,超时走重试分支 accounts: list[Account] = field(default_factory=list) def load_config(path: str | Path) -> AppConfig: raw = yaml.safe_load(Path(path).read_text(encoding="utf-8")) # accounts 单独 pop 出来,避免和 AppConfig 的其它字段混在一起传给构造函数 accounts = [Account(**item) for item in raw.pop("accounts", [])] return AppConfig(accounts=accounts, **raw)

match_threshold这个参数值得单独拎出来讲:它是整个项目里最像玄学的数字。调到 0.95,换个分辨率或者游戏更新了美术资源就匹配不上,程序卡在原地;调到 0.7,界面里长得像的按钮会被误认,点错地方。我一般从 0.85 起步,用tools/里的标定脚本把每个模板图的最低得分打出来,取「正确命中最低分」和「误命中最高分」的中点。

3.2 窗口绑定与截图:客户区坐标是唯一可靠坐标

截图这块最容易翻车的地方是坐标系。屏幕坐标、窗口坐标、客户区坐标是三套东西,混用一次就要 debug 一晚上。这个包的约定是:所有内部逻辑统一用客户区坐标,只有在真正发鼠标事件的那一刻才转成屏幕坐标。

import win32gui, win32ui from ctypes import windll def find_window(keyword: str) -> int: hits = [] def _cb(hwnd, _): # 必须过滤可见窗口,否则同名隐藏窗口会排在前面被误选 if win32gui.IsWindowVisible(hwnd) and keyword in win32gui.GetWindowText(hwnd): hits.append(hwnd) win32gui.EnumWindows(_cb, None) if not hits: raise RuntimeError(f"没找到标题包含「{keyword}」的窗口") return hits[0] def client_rect(hwnd: int) -> tuple[int, int, int, int]: # 返回客户区宽高,不含标题栏和边框 left, top, right, bottom = win32gui.GetClientRect(hwnd) return left, top, right - left, bottom - top def grab_client(hwnd: int): """按客户区截图,返回 BGR ndarray""" _, _, w, h = client_rect(hwnd) hwnd_dc = win32gui.GetWindowDC(hwnd) mfc_dc = win32ui.CreateDCFromHandle(hwnd_dc) save_dc = mfc_dc.CreateCompatibleDC() bmp = win32ui.CreateBitmap() bmp.CreateCompatibleBitmap(mfc_dc, w, h) save_dc.SelectObject(bmp) # 第三个参数 1 表示 PW_CLIENTONLY,只抓客户区 windll.user32.PrintWindow(hwnd, save_dc.GetSafeHdc(), 1) # ... 取出 bmp 的位数据转 numpy,再 cvtColor 成 BGR win32gui.DeleteObject(bmp.GetHandle()) save_dc.DeleteDC() mfc_dc.DeleteDC() win32gui.ReleaseDC(hwnd, hwnd_dc)

PrintWindow是首选,因为它不受窗口遮挡影响,你可以把游戏窗口丢到后台照样截图。但它是双刃剑:如果客户端用的是硬件加速渲染,PrintWindow经常返回一张纯黑图——这个坑在排查那章细说,包里留了一个降级分支,检测到全黑就改用屏幕区域截图 + 前台激活。

3.3 模板匹配定位:阈值、多目标与大图性能

定位按钮就是一次cv2.matchTemplate,但细节决定了它是能用还是好用。

import cv2 import numpy as np def locate(screen: np.ndarray, tpl_path: str, threshold: float): tpl = cv2.imread(tpl_path) if tpl is None: raise FileNotFoundError(tpl_path) # TM_CCOEFF_NORMED 对整体亮度变化不敏感,适合游戏里带渐变的按钮 res = cv2.matchTemplate(screen, tpl, cv2.TM_CCOEFF_NORMED) _, max_val, _, max_loc = cv2.minMaxLoc(res) if max_val < threshold: return None h, w = tpl.shape[:2] # 返回模板中心点,点击时比左上角稳定,不会因为模板裁大了一点就点偏 return max_loc[0] + w // 2, max_loc[1] + h // 2, float(max_val)

三个参数说明。tpl_path对应的模板图要裁得紧,四周留白越多,匹配得分越容易被无关像素稀释。threshold见上一节。TM_CCOEFF_NORMED这个算法选型是因为游戏 UI 常有高亮态、按下态,亮度会变但结构不变,归一化相关系数对亮度线性变化免疫,比平方差稳健得多。

性能上有个容易忽略的点:全屏 1920×1080 匹配一张 200×60 的模板,大概 30~50 毫秒,一回合调用十几次就上去了。包里的做法是先裁 ROI——UI 按钮的位置基本固定,按配置里的区域先切一刀,再在小图上匹配,能压到 3 毫秒以内。

3.4 状态机主循环:把「现在该点什么」写成转移表

登录流程天然是个状态机:启动画面 → 登录框 → 选服 → 角色界面 → 主城。包里用枚举加字典转移表来写,而不是一堆 if-else 堆在一个 while 里。

from enum import Enum, auto import random, time class State(Enum): BOOT = auto() LOGIN_PANEL = auto() SERVER_LIST = auto() CHARACTER = auto() MAIN_CITY = auto() BATTLE = auto() UNKNOWN = auto() def run(self): while not self.stop_flag: screen = self.driver.grab() # 一帧截图 state = self.recognizer.classify(screen) # 分类,内部就是多次 locate handler = self.handlers.get(state) if handler is None: # UNKNOWN 不要死循环,退化为「等一秒再看」,给动画留时间 time.sleep(1.0) continue started = time.monotonic() try: handler(screen) except Exception as exc: self.log.warning("状态 {} 处理失败:{}", state.name, exc) self.recover() # 单状态超时保护,防止某个动画卡住把整个登录器挂死 if time.monotonic() - started > self.cfg.action_timeout: self.log.warning("状态 {} 超时,触发恢复", state.name) self.recover() time.sleep(random.uniform(0.15, 0.35)) # 人味延迟,别把 CPU 打满

classify的实现就是按优先级依次locate每个状态的标志性模板,命中即返回。注意顺序有讲究:对战界面的元素最多、最容易误命中,所以放后面;登录面板的标志物最独特,放前面。

random.uniform(0.15, 0.35)这个延迟不是为了规避检测这类目的,纯粹是因为回合制游戏有转场动画,点太快等于点在动画中间,事件会被吞掉。这也是我踩过的坑:去掉这行,登录成功率从 95% 掉到 60% 上下。

recover()里做的事很土:把窗口拉到前台、按一次 ESC 退回上一层、重新截一张图确认。写自动化最怕的就是「不知道自己在哪个界面」,恢复策略比正常流程更值得花时间。

4. 强化学习环境封装:把一回合对战变成 Gym 接口

算法层是这个包真正有意思的地方。它没有从零写一个 PPO,而是老老实实把游戏封装成 gymnasium 环境,剩下的交给 stable-baselines3。这个决策很对:环境封装的正确性是你唯一能控制的东西,算法实现交给成熟库,出问题的时候能把变量减到最少。

4.1 观测、动作、奖励三件套

先看环境骨架。观测是 14 维向量,全部归一化到 0~1 之间;动作是离散 9 个:4 个技能 + 4 个换精灵 + 1 个逃跑。

import gymnasium as gym from gymnasium import spaces import numpy as np class BattleEnv(gym.Env): metadata = {"render_modes": ["human"]} def __init__(self, driver, cfg): super().__init__() self.driver = driver # 复用登录器那套截图/点击能力 self.cfg = cfg self.max_hp = cfg.max_hp # 满血值,用来归一化 self.max_turns = 60 # 一局最多 60 回合,超时截断 self.turn = 0 # 4 技能 + 4 换人 + 1 逃跑 self.action_space = spaces.Discrete(9) # 观测:[己方hp比, 己方存活数比, 4个pp比, 敌方hp比, 敌方存活数比, # 回合进度, 上回合造成伤害比, 上回合受到伤害比, 是否有属性克制] self.observation_space = spaces.Box( low=0.0, high=1.0, shape=(14,), dtype=np.float32 ) def reset(self, *, seed=None, options=None): super().reset(seed=seed) self.driver.ensure_battle() # 点「开始对战」,直到进入战斗界面 self.turn = 0 self.prev = self.driver.snapshot() # 结构化快照:双方血量、存活数、PP return self._obs(self.prev), {} def _obs(self, snap) -> np.ndarray: return np.array([ snap["self_hp"] / self.max_hp, snap["self_alive"] / 4.0, *[pp / self.cfg.max_pp for pp in snap["self_pp"]], snap["enemy_hp"] / self.max_hp, snap["enemy_alive"] / 4.0, self.turn / self.max_turns, snap["last_dmg_dealt"] / self.max_hp, snap["last_dmg_taken"] / self.max_hp, 1.0 if snap["counter"] else 0.0, ], dtype=np.float32)

_obs里所有量都除了一个基准值,这不是可选项。血量除以满血、回合除以上限、PP 除以最大 PP——如果不归一化,14 维里有的在 0~999,有的在 0~1,策略网络的第一层权重会被大数值维度主导,训练曲线会呈现出一种「看起来在学但学不会」的状态。

snapshot()是个抽象方法,包里给了两个实现:一个是纯图像的(从血条像素长度反推百分比),一个是半结构化的(先用模板匹配定位数值区域,再做简单的数字识别)。前者鲁棒但精度差,后者精度高但依赖 UI 稳定。二次开发时你可以换成任何你喜欢的解析方式,只要返回同样的字典结构。

4.2 step 实现:奖励设计和回合结束判定

这部分是整个环境里最容易写错的地方,也是我最想强调的。

def step(self, action): before = self.prev self.driver.do_action(action) # 真正落到 UI 的一次点击 after = self.driver.wait_turn_end() # 轮询截图,直到画面连续 3 帧稳定 self.turn += 1 reward = 0.0 dmg_dealt = before["enemy_hp"] - after["enemy_hp"] dmg_taken = before["self_hp"] - after["self_hp"] # 攻守不对称:打出伤害权重 1.0,挨打惩罚 0.6。 # 完全对称会让策略倾向于摆烂拖回合,因为挨打和打人"净值"一样 reward += dmg_dealt / self.max_hp * 1.0 reward -= dmg_taken / self.max_hp * 0.6 # 击杀奖励要显著大于普通伤害,否则策略学不会"优先补刀" if after["enemy_alive"] < before["enemy_alive"]: reward += 0.8 if after["self_alive"] < before["self_alive"]: reward -= 0.8 terminated = False if after["enemy_alive"] == 0: reward += 3.0 terminated = True elif after["self_alive"] == 0: reward -= 3.0 terminated = True # 超时截断:不终止但计算 bootstrap,否则价值网络学不到长期收益 truncated = self.turn >= self.max_turns and not terminated if truncated: reward -= 0.5 # 每回合小额步数惩罚,抑制无意义的换人循环 reward -= 0.02 self.prev = after return self._obs(after), float(reward), terminated, truncated, {}

几个参数值得解释。dmg_dealt和dmg_taken的权重比 1.0 : 0.6 是调出来的,不是算出来的:早期试过 1 : 1,训练出来的策略会在血量健康时不停换精灵消耗回合,因为「不打人也不挨打」在对称奖励下不亏。改成非对称后行为明显积极。

击杀奖励 0.8 和胜负奖励 3.0 的量级关系也很关键。如果终结奖励给到 10 以上,价值函数会被稀疏的大奖励主导,前 50 个回合的梯度信号被压没;给到 1 以下,又会出现「明明能赢却选择逃跑省回合」的退化策略。

truncated必须和terminated分开。这是 Gym 接口里最容易写错的一个点:超时不是环境终止,只是这一局不继续了,价值网络需要 bootstrap。写混了,训练出来的价值估计会系统性偏低。

wait_turn_end用「连续 3 帧画面差异小于阈值」判定动画结束,比固定 sleep 靠谱得多,也是采样吞吐的关键——固定 sleep 2 秒和实际等待 0.8 秒,采样效率差两倍多。

4.3 向量化训练:先跑通单环境,再谈并行

训练入口就是标准 SB3 那一套:

from stable_baselines3 import PPO from stable_baselines3.common.vec_env import DummyVecEnv, VecNormalize def make_env(): driver = build_driver(cfg) return BattleEnv(driver, cfg) # 注意:单窗口只能有一个实例,这里必须用 DummyVecEnv 串行,不能 SubprocVecEnv venv = DummyVecEnv([make_env]) venv = VecNormalize(venv, norm_obs=True, norm_reward=True, clip_obs=10.0) model = PPO( "MlpPolicy", venv, n_steps=256, # 每轮采样步数,一局 60 回合,取 256 约 4 局 batch_size=64, learning_rate=3e-4, gamma=0.99, # 回合制战略性强,折扣因子给高 ent_coef=0.01, # 小熵正则,防止过早收敛到"只放一号技能" verbose=1, ) model.learn(total_timesteps=200_000) model.save("ckpt/battle_ppo")

n_steps=256这个值配合一局 60 回合的上限,意味着每轮更新大概收集 4 局经验。太小则梯度方差大,太大则采样成本高——在这个项目里采样是走真实 UI 的,一回合 2 秒,256 步就是 8 分钟,这个量级必须精打细算。

VecNormalize几乎是必须的。观测虽然手工归一化了,但奖励的量级会随对手强度漂移,归一化后学习率才不用跟着调。注意保存模型时要一起存venv.save(),否则评估阶段归一化参数对不上,表现会断崖式下跌。

DummyVecEnv而不是SubprocVecEnv是个硬约束:一个游戏窗口只能被一个进程操作,开多进程会互相抢焦点,点击全乱。想提吞吐只能开多个窗口实例,这属于二次开发范畴,第 6 章再说。

5. 避坑与排查:跑不起来时先看这五条

这个包在别人的机器上大概率不会一次跑通,我把同类项目里反复出现的坑按「现象 → 原因 → 解决」整理了一遍,按出现频率排序。

5.1 截图全是黑的

现象:grab_client返回的 ndarray 全 0,或者只有窗口边框。

原因:客户端用了硬件加速渲染,PrintWindow对这类窗口拿不到后台缓冲,返回黑图。这跟系统版本、显卡驱动都有关,换台机器可能就好了,很玄学。

解决:先做全黑检测,screen.mean() < 1.0就判定失败,降级到「SetForegroundWindow+ 屏幕区域截图」。包里capture.py留了这个分支。另外把游戏设置里的渲染模式改成软件渲染也能绕过,但会掉帧,影响点击时序。

5.2 点下去没反应,日志显示点击成功

现象:click()正常返回,游戏毫无反应。

原因:窗口没有焦点。pyautogui发的是系统级鼠标事件,落在最前面的窗口上,如果游戏在后台,点击就落到别的窗口了。另外SetForegroundWindow有系统限制,不是每次调用都生效。

解决:三步走——先SetForegroundWindow,再Sleep(150ms)等焦点切换完成,然后用win32gui.GetForegroundWindow()校验一下当前前台窗口是不是目标句柄,不匹配就重试两三次。别省这一步,我见过因为少了这个校验而白跑一整晚训练的。

5.3 坐标全偏,点击落在按钮外

现象:在人家的机器上跑得好好的,换到 125% 或 150% 缩放的屏幕上,所有点击都往右下偏。

原因:Windows 的 DPI 虚拟化。程序以为自己是 100% 缩放,拿到的客户区尺寸和实际渲染尺寸不一致,坐标换算全错。

解决:在程序启动的第一行调用 DPI 感知声明:

from ctypes import windll # 0 = UNAWARE, 1 = SYSTEM_AWARE, 2 = PER_MONITOR_AWARE # 必须在任何窗口创建前调用,晚了无效 try: windll.shcore.SetProcessDpiAwareness(2) except AttributeError: windll.user32.SetProcessDPIAware()

调用之后重新标定一次坐标,模板图也要按新分辨率重新裁。这是我踩过最亏的一次坑:花了两小时怀疑匹配算法,最后发现是 DPI。

5.4 训练不收敛,胜率一直在 30% 上下抖

现象:ep_rew_mean曲线先涨后平,评估胜率不比随机策略高多少。

原因:三种可能,按概率排——观测里的某个维度没归一化;奖励尺度差异过大(终结奖励 10、步惩罚 0.02,差 500 倍);gamma设太低,回合制需要长视野。另外还有一个隐蔽原因:wait_turn_end判定的「画面稳定」不可靠,偶尔在动画中间就返回了,导致观测和实际状态错位。

解决:先把环境跑成随机策略,采样 20 局把info里的各项奖励分量打出来看分布,看哪个分量占主导。然后用VecNormalize统一奖励尺度,把终结奖励压到 3~5 的量级。最后修wait_turn_end,把连续稳定帧数从 2 提到 3,并加上最短等待时间。

5.5 打包之后模板图找不到

现象:源码跑得好好的,PyInstaller打出来的 exe 一启动就报FileNotFoundError,指向conf/templates/xxx.png。

原因:打包后资源文件被解到临时目录sys._MEIPASS,用相对路径读不到。另外cv2和win32的隐式导入 PyInstaller 经常分析不出来。

解决:统一走一个resource_path()函数,判断getattr(sys, "frozen", False)来切换根路径;build.spec里用--add-data "conf;conf"带上整个配置目录,hiddenimports补上win32timezone等模块。打包这件事没有后悔药,第一次就要做对。

6. 二次开发:把登录器改成自己的实验平台

这个包最大的价值不在「能用」,而在「改起来有明确的接缝」。我如果拿它做二次开发,会从三个方向动手,按投入产出比排序。

第一个方向是把解析层抽成适配器。现在的snapshot()和locate()混在一起,想换一种解析方式就得改环境代码。应该定义一个StateParser抽象类,图像方案、结构化方案、甚至以后接入别的数据源,都实现同一个parse(screen) -> dict接口,环境只依赖这个接口。这样 UI 一更新,你只需要重写一个解析器,而不是动整个env.py。

第二个方向是扩动作空间和加课程学习。现在 9 个离散动作是写死的,实际对战里技能组合、道具使用会让空间大很多。我一般会把动作拆成「目标选择 + 技能选择」两层,用MultiDiscrete表示,配合ActionMasker屏蔽掉当前不可用的动作(PP 为 0、已阵亡的精灵),这比让模型自己学会「这个动作无效」高效得多。课程学习那边,从「1v1 固定对手」开始,逐步过渡到「3v3 随机阵容」,胜率曲线会好看很多。

第三个方向是提采样吞吐,但这里有硬约束:一个窗口只能被一个进程操作。可行的做法是开多个窗口实例,每个进程绑定一个hwnd,用SubprocVecEnv并行。代价是内存和窗口管理复杂度直线上升,我建议先单环境把策略跑通,确认奖励设计没问题,再考虑并行——顺序反了,你会在调试「到底是环境 bug 还是并行 bug」上耗掉一周。

验证方法我给一个我自己常用的表格对比:

评估项做法合格线
规则基线固定「血量最高就放最强技能」的脚本 AI必须先跑出来,作为下界
随机基线均匀随机选动作,跑 100 局胜率约等于动作数倒数
训练策略固定随机种子,跑 100 局统计胜率显著高于规则基线
稳定性同一策略分 3 个时间段各跑 30 局胜率波动小于 10 个百分点

固定随机种子这件事必须做,否则你看到的所有提升都可能是噪声。我吃过这个亏:有一次换了奖励权重,胜率从 45% 涨到 62%,高兴了一下午,后来发现是那批对手阵容恰好偏弱,固定种子重跑之后只涨了 3 个点。

从那以后我每次动奖励函数或者改观测,都强制先把规则基线跑满 100 局、把种子固定、把三个基线指标一起打出来,看不到对比数字不改代码。这个习惯救过我好几次,也希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 6:55:00

Frenet框架下人工势场法的自动驾驶轨迹规划优化

1. 项目概述&#xff1a;当人工势场法遇上Frenet坐标系在自动驾驶和机器人运动规划领域&#xff0c;轨迹规划算法始终面临着复杂环境下的路径最优性与安全性平衡难题。这个项目提出了一种融合人工势场法的Frenet框架轨迹规划方案&#xff0c;特别针对弯道场景进行了优化。我在实…

作者头像 李华
网站建设 2026/11/2 6:54:32

金融行业网络钓鱼攻击防御与AI技术应用

1. 金融行业网络钓鱼攻击的现状与挑战金融行业一直是网络钓鱼攻击的重灾区。根据最新统计数据显示&#xff0c;超过60%的网络钓鱼攻击针对金融机构及其客户。攻击者通过伪造银行网站、发送虚假交易提醒等手段&#xff0c;诱骗用户泄露敏感信息。这类攻击不仅造成直接经济损失&a…

作者头像 李华
网站建设 2026/11/2 6:54:14

LoRA技术:降低大模型微调成本的关键方法

1. 项目概述&#xff1a;LoRA技术如何降低大模型微调成本最近两年大模型技术爆发式发展&#xff0c;但显存需求也随之水涨船高。以主流的7B参数模型为例&#xff0c;全参数微调需要至少80GB显存&#xff0c;这让普通开发者和中小企业望而却步。LoRA&#xff08;Low-Rank Adapta…

作者头像 李华
网站建设 2026/11/2 6:53:34

本科论文写作工具全攻略:从文献管理到查重降重

1. 论文写作工具现状与选择逻辑本科阶段的论文写作往往面临三大核心痛点&#xff1a;文献管理混乱、写作效率低下、格式规范难以把握。传统写作模式下&#xff0c;学生需要手动整理上百篇文献&#xff0c;反复调整引用格式&#xff0c;同时还要应对查重降重的压力。而AI工具的介…

作者头像 李华
网站建设 2026/11/2 6:53:31

C++ Traits编程:类型特性与编译期优化的艺术

1. 什么是Traits及其核心价值在泛型编程的世界里&#xff0c;Traits&#xff08;特性&#xff09;是一种强大的元编程技术&#xff0c;它允许我们在编译期间对类型进行查询和操作。简单来说&#xff0c;Traits就像是为类型设计的"体检报告"&#xff0c;能够告诉我们某…

作者头像 李华
网站建设 2026/11/2 6:53:19

免注册调用大漠插件:.NET Core 5.0 下的COM互操作实战

简介&#xff1a;这份资源面向使用 C# 开发 Windows 桌面自动化工具的开发者&#xff0c;聚焦在 .NET Core 5.0 与 WinForms 环境下免注册调用大漠插件&#xff08;dm.dll&#xff09;这一常见痛点&#xff0c;帮助读者绕开注册表写入与权限限制&#xff0c;完成找字、找图、截…

作者头像 李华