☰
逆强化学习实战:从专家轨迹反推奖励与Java实现
2026/10/27 10:37:18 网站建设 项目流程

简介:这是一份面向强化学习与逆强化学习(IRL)方向学习者、研究者的示例代码包,围绕在 BURLAP 代码库基础上搭建 IRL 框架展开,适合已具备 Java 与强化学习基础、希望从可运行示例理解 apprenticeship learning、网格世界与多智能体性能对比的读者。压缩包共 497 个文件,以 484 个 Java 源码为主体,辅以 XML 配置、Maven 的 pom 文件及若干 jar 依赖,整体约 2.29MB,目录中保留了 BURLAP 快照与自建 IRL 框架所需改动,便于对照阅读框架接入与算法实现。已有 1048 人学习下载,说明其在相关主题中具有一定参考热度。读者可从中获得逆强化学习示例的工程结构、依赖组织与实验入口,并借助 GridWorld、ApprenticeshipLearning 等模块理解从环境建模到学习流程的落地方式。对希望阅读源码、复现实验或在此基础上扩展算法的人来说,包内示例覆盖环境建模、单阶段正规形式博弈、性能绘图与学徒学习等环节,可作为课程设计、论文预研或项目原型的参考底稿。

1. 从专家轨迹反推奖励:一份能跑通的逆强化学习示例代码

奖励函数写不对,策略怎么调都是在错误目标上做优化,这是做强化学习最费时间的一段。逆强化学习把方向反过来:不猜奖励,从专家轨迹里把奖励反推出来。这份 IRLTutorial 的示例代码,就是把这件事从公式落成可运行的骨架——轨迹采样、特征期望统计、奖励权重求解、策略更新,四段循环串成一个闭环。它对 Java 技术栈的读者同样友好,因为主干是矩阵运算加一个线性规划或梯度求解,不是某个框架的 API 拼装。适合三类人:做过基础 RL 想搞清 IRL 数学的、要复现相关论文的、以及需要在 JVM 项目里嵌一套奖励推断逻辑的。

2. 逆强化学习在解什么方程:从专家轨迹反推奖励函数

2.1 正向 RL 与逆向 RL 差在哪一步

正向强化学习的输入是一个五元组:状态空间、动作空间、转移概率、折扣因子、奖励函数,输出是最优策略。逆强化学习把奖励函数从输入里拿掉,换成一批专家轨迹,输出变成奖励函数本身,然后才用它去跑正向 RL 求策略。差别看着只有一项,实际改变了整个问题的性质:正向 RL 是"给定目标求最优解",逆强化学习是"从行为反推目标",后者天然是一个双层优化问题——内层给定奖励求最优策略,外层调整奖励让专家策略在该奖励下最优。示例代码里之所以会出现嵌套循环,根源就在这里。

新手最容易在这里走偏:把专家轨迹当成监督学习的标签,直接拿状态做输入、动作做输出训一个分类器。那是行为克隆,学的是策略不是奖励,遇到训练集里没出现过的状态就崩,换个环境还得重来。逆强化学习多花的那部分计算,买的就是"奖励可以迁移"这件事。

对比项正向 RL逆强化学习
已知输入环境模型 + 奖励函数环境模型 + 专家轨迹
求解目标最优策略奖励函数(或其参数)
主要难点探索与信用分配奖励的不适定性
失败表现收敛慢、方差大奖励权重退化、策略偏离专家

这张表里最值得盯的是"不适定性"那一行。给定一批专家轨迹,能解释它的奖励函数有无穷多个,最极端的情况是全零奖励——任何策略在该奖励下都同样最优,专家轨迹自然也被"解释"了。所以真实的 IRL 算法都必须加约束,这也是后面所有数学推导的出发点。

2.2 特征期望匹配:IRL 的最小可行形式

要让问题有唯一解,最常见的做法是把奖励限制成特征的线性组合:R(s) = wᵀφ(s)。φ(s) 是人工设计的特征向量,比如到目标的距离、与障碍物的邻近度、动作代价、朝向偏差,w 是待求的权重。这个假设不完美,但它把"求一个函数"降级成"求一组权重",从不可解变成可解,工程上够用。

在奖励线性可表示的前提下,策略的优劣可以用特征期望来刻画:μ(π) = E_π[Σ γᵗ φ(s_t)],也就是沿策略轨迹对特征做折扣加权求和。特征期望是个向量,维度等于特征数,通常远小于状态空间,这就把高维的策略比较压缩成了低维向量比较。经典的最大间隔形式写成线性规划:

maximize t subject to wᵀμ(π_E) - wᵀμ(π) ≥ t 对任意策略 π ||w||₂ ≤ 1

它的含义很直白:找一组权重,让专家策略在特征期望上至少领先其他所有策略 t 这么多。约束数量等于策略空间大小,实际不可能全枚举,标准做法是用策略迭代在每轮只生成当前最优策略、往约束集里加一条,这就成了"基于策略迭代的逆强化学习"。

另一个更常用的形式是最大熵逆强化学习。它假设轨迹服从玻尔兹曼分布 P(τ|w) ∝ exp(wᵀμ(τ)),对 w 求负对数似然的梯度,得到的表达式干净得让人意外:

∇w = μ(π_E) - E_{π_w}[μ]

就是"专家特征期望减去当前策略特征期望"。当前策略的特征期望已经追平专家,梯度就是零,权重不再更新。这个形式在代码里实现成本最低,不需要线性规划求解器,一次采样加一次向量减法就能算梯度,示例代码里优先用它是有道理的。

2.3 示例代码的四段式骨架

不管用线性规划还是梯度法,代码结构都是四段。把这张表记住,后面读任何一份 IRL 实现都能对上号。

阶段做什么主要输入主要输出出问题的典型表现
轨迹采样用当前策略与环境交互当前策略、环境、采样条数状态动作序列策略早熟,样本全是同一条路径
特征统计计算 φ(s) 并折扣加权轨迹、特征函数、γ特征期望向量特征量纲差异大,权重被某一维主导
奖励求解更新 w(LP 或梯度)专家特征期望、当前特征期望新的 w权重全零或数值爆炸
策略更新用 R(s)=wᵀφ(s) 重跑正向 RL新奖励、环境新策略奖励变了策略没变,内外层脱节

四段里最容易被轻视的是第二段。特征统计看着只是求和,但折扣因子 γ 怎么取、要不要做归一化、终止状态的特征算不算进去,每一个都会实打实改变结果。γ 取 0.99 时一条 200 步的轨迹有效长度约 100 步,取 0.9 时只剩 10 步,专家轨迹里靠后的信息基本被丢掉。我一般先在 0.95 附近试,然后看特征期望向量的余弦相似度变化,而不是只看回报曲线。

第三段和第四段之间还有一个隐性耦合:奖励更新幅度太大,内层策略会直接跳到完全不同的区域,外层梯度方向就失去意义;更新幅度太小,跑几百轮 w 还在原地。学习率或者 LP 的间隔参数 t 在这里起的作用,和普通梯度下降里的步长是一回事,但因为它作用在双层结构上,容忍度低得多。

3. 把示例代码跑起来:依赖对齐与一次最小闭环

3.1 先看入口,再装环境

拿到一份教程示例代码,第一件事不是 pip install,是先判断代码的组织方式。常见的有三种:单文件脚本、带配置文件的多模块工程、notebook 集合。判断方法很土但有效——找带 main、run、experiment、train 字样的文件,看有没有 yaml/json/toml 配置,看有没有 requirements.txt、environment.yml 或者 pom.xml。这一步花五分钟,能省掉后面半小时的环境报错。

# 在代码根目录执行,先摸清结构再动手 ls -la # 找可能的入口脚本 find . -maxdepth 2 -type f \( -name "*main*" -o -name "*run*" -o -name "*train*" \) -print # 找依赖声明文件,决定用哪种方式装环境 find . -maxdepth 2 -type f \( -name "requirements*.txt" -o -name "environment*.yml" -o -name "pom.xml" -o -name "build.gradle" \) -print # 看有没有硬编码的路径和随机种子,这决定了能不能复现 grep -rn "seed\|random_state" --include="*.py" . | head -20

第一条命令看目录层级,判断是不是多模块;第二条找入口,注意有些仓库的入口藏在 examples 或 scripts 子目录里;第三条确认依赖管理方式,有 environment.yml 就优先用 conda,有 pom.xml 说明仓库里有 JVM 侧的实现,这和后面的 Java 路线直接相关;第四条查随机种子,没有固定种子的实现,同一份代码跑两次结果能差出百分之十几,排错时会让人怀疑人生。

3.2 数值栈与求解器的版本对齐

逆强化学习的代码对数值库版本比一般脚本敏感,因为它要做线性规划和矩阵求逆,不同版本的求解器在小数点后几位上的差异,经过几十轮迭代放大后会变成完全不同的权重。

依赖作用常见问题
numpy矩阵运算、特征统计1.x 与 2.x 的部分隐式类型转换行为不同
scipylinprog 等优化求解器默认求解器切换后,同等问题的解在小数位上不完全一致
gym / gymnasium环境接口reset 返回值数量变化,老代码直接报解包错误
matplotlib可视化曲线只影响画图,不影响结果,可以最后装
# 建独立环境,避免和系统里的科学计算栈打架 python -m venv irl_env source irl_env/bin/activate # Windows 用 irl_env\Scripts\activate # 先装数值底座,再装环境包,最后装可视化 pip install "numpy>=1.24,<2.0" "scipy>=1.10" pip install gymnasium # 新代码用 gymnasium,不要再装老的 gym pip install matplotlib # 装完立刻验证求解器可用,别等到跑主程序才发现 python -c "import numpy, scipy; from scipy.optimize import linprog; print(numpy.__version__, scipy.__version__); print(linprog([-1,-1], A_ub=[[1,2]], b_ub=[4]).x)"

这里把 numpy 卡在 2.0 以下是有意的,示例代码里那种np.float、隐式 int 转 float 的老写法在 2.x 下会直接抛错,改代码的时间比降版本贵。最后一行是自检:linprog的最小化目标取负号求最大化,约束x+2y≤4,能打印出一组可行解说明求解器链路是通的。如果这一步就报错,问题在环境不在代码,别往下耗。

提示:如果仓库里同时有 requirements.txt 和 environment.yml,以能装成功的那份为准,另一份通常已经过期。不要两份混着装。

3.3 GridWorld 上的第一次闭环

环境检查完,别急着跑完整实验,先在一个小到能手工验算的网格世界上确认四段循环是通的。下面这段脚本是我用来做首轮验证的,5×5 网格、两个特征(到目标的曼哈顿距离、是否越界),专家策略走最短路,然后用最大熵的梯度形式更新权重。

import numpy as np np.random.seed(0) SIZE = 5 GOAL = (4, 4) GAMMA = 0.95 LR = 0.05 STEPS = 400 def features(state): """两个特征:到目标的距离(越小越好)、越界惩罚标记""" r, c = state dist = abs(r - GOAL[0]) + abs(c - GOAL[1]) return np.array([-dist, 0.0]) # 越界特征在正常状态恒为 0 def expert_trajectory(): """专家:沿曼哈顿最短路从 (0,0) 走到目标""" traj, state = [], (0, 0) while state != GOAL: traj.append(state) r, c = state state = (r + 1, c) if r < GOAL[0] else (r, c + 1) traj.append(GOAL) return traj def feature_expectation(traj): """折扣加权的特征期望""" total = np.zeros(2) for t, s in enumerate(traj): total += (GAMMA ** t) * features(s) return total def random_policy_trajectory(): """当前策略:带一定概率走反方向的随机游走,模拟未收敛的策略""" traj, state = [], (0, 0) for _ in range(20): traj.append(state) if state == GOAL: break r, c = state moves = [] if r > 0: moves.append((r - 1, c)) if c > 0: moves.append((r, c - 1)) if r < SIZE - 1: moves.append((r + 1, c)) if c < SIZE - 1: moves.append((r, c + 1)) state = moves[np.random.randint(len(moves))] traj.append(state) return traj mu_expert = feature_expectation(expert_trajectory()) w = np.array([0.0, 0.0]) for i in range(STEPS): mu_policy = feature_expectation(random_policy_trajectory()) grad = mu_expert - mu_policy # 最大熵 IRL 的梯度形式 w += LR * grad print("expert mu:", mu_expert) print("learned w:", w) print("cosine:", float(np.dot(w, mu_expert) / (np.linalg.norm(w) * np.linalg.norm(mu_expert))))

这段代码的核心就一行:grad = mu_expert - mu_policy。它是最大熵逆强化学习负对数似然对权重的梯度,含义是当前策略的特征期望离专家差多少,就往哪个方向调。GAMMA控制折扣强度,0.95 意味着一百步之后的信息权重降到 0.6%;LR是步长,0.05 在这个量级下大概几百轮收敛;STEPS不是越大越好,超过一千轮之后 w 的方向基本不动,只是绝对值在涨,这时看余弦相似度比看权重绝对值更可靠。

跑完应该看到 w 的第一个分量是负数——距离特征本身是负值,负权重等于把它翻正,说明"距离目标越近越好"这个方向学到了。第二个分量因为没有越界样本参与,会一直停在 0 附近,这恰好暴露一个真实问题:专家轨迹覆盖不到的特征,权重永远学不出来。这个现象在复杂环境里会变成严重的偏差,第 5 章会专门讲。

4. 用 Java 复现主循环:矩阵运算、线性规划与精度对齐

4.1 为什么 JVM 侧要自己搭数值层

很多人第一反应是问这套东西能不能用 Java 写。能写,但要接受一个前提:JVM 上没有和 scipy.optimize 对等的一站式求解器,得自己拼装。Apache Commons Math 提供了线性代数和单纯形法求解器,EJML 在矩阵乘法上更快但只做代数不做优化,两者的定位是互补的。选型的判断标准很简单——如果只做最大熵那条梯度路线,只需要矩阵乘法和向量运算,EJML 或者裸double[][]就够;如果要做最大间隔的线性规划形式,必须上 Commons Math 的 SimplexSolver,而且得自己把约束矩阵维护好。

方案覆盖能力适用场景代价
裸 double[][]手写矩阵乘加特征维度小于 50代码量大,易出下标错误
Apache Commons Math线性代数 + 单纯形求解需要 LP 或需要 QR/SVD依赖较重,API 偏啰嗦
EJML高性能线性代数特征维度上百、采样量大不含优化器,需另配

我一般的做法是先用 Commons Math 把整条链路跑通,确认权重方向和 Python 版一致,再决定要不要为了性能换成 EJML。反过来先优化性能,往往到最后发现瓶颈在采样不在矩阵运算。

<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-math3</artifactId> <version>3.6.1</version> </dependency>

这段依赖声明只引一个包,够用。版本号跟着仓库实际使用的走,不要随手升到最新,Commons Math 在 3.x 内部有过优化器 API 的重构,跨小版本可能出现类找不到的情况。

4.2 特征期望统计的 Java 实现

特征期望的计算在数学上就是矩阵乘法:把每条轨迹上的特征向量按折扣权重累加,再对所有轨迹取平均。写成矩阵形式是 Φᵀd,Φ 是状态特征矩阵(行是状态、列是特征),d 是折扣访问频率向量。

import org.apache.commons.math3.linear.*; public class FeatureExpectation { /** 折扣加权,把一条轨迹压成一个特征期望向量 */ public static RealVector fromTrajectory(double[][] feats, double gamma) { int dim = feats[0].length; RealVector mu = new ArrayRealVector(dim); double discount = 1.0; for (double[] f : feats) { mu = mu.add(new ArrayRealVector(f).mapMultiply(discount)); discount *= gamma; // 逐时刻衰减,注意放在累加之后 } return mu; } /** 多条轨迹取算术平均,样本量越大估计越稳 */ public static RealVector average(java.util.List<RealVector> mus) { RealVector sum = new ArrayRealVector(mus.get(0).getDimension()); for (RealVector m : mus) { sum = sum.add(m); } return sum.mapDivide(mus.size()); } /** 梯度:专家特征期望减去当前策略特征期望 */ public static RealVector gradient(RealVector muExpert, RealVector muPolicy) { return muExpert.subtract(muPolicy); } public static void main(String[] args) { double[][] expertFeats = {{-8, 0}, {-6, 0}, {-4, 0}, {-2, 0}, {0, 0}}; double[][] policyFeats = {{-8, 0}, {-7, 0}, {-5, 1}, {-3, 1}, {-1, 0}}; RealVector muE = fromTrajectory(expertFeats, 0.95); RealVector muP = fromTrajectory(policyFeats, 0.95); System.out.println("mu_expert = " + muE); System.out.println("gradient = " + gradient(muE, muP)); } }

fromTrajectory里折扣的施加顺序是个隐蔽的坑:discount必须先乘到当前特征上,再自乘衰减,写反了整条轨迹的权重会整体偏移一位,结果看起来"差不多"但方向可能反了。average之所以单独抽出来,是因为单条轨迹的特征期望方差很大,实际实现里每条轨迹都要重采样多次,平均之后梯度才稳。gradient那个方法看着只有一行,但它是整个 Java 版本和 Python 版本对齐的锚点——只要两边的mu_expert和mu_policy数值一致,梯度必然一致,不需要逐行对照循环。

4.3 线性规划与梯度法的替换

如果教程的示例代码用的是最大间隔形式,迁到 Java 就要处理线性规划。Commons Math 的SimplexSolver求解的是标准形,需要把"最大化 t"和一堆不等式约束手工组装成LinearObjectiveFunction加LinearConstraintSet。真正麻烦的不是组装,是约束集在每轮策略迭代中动态增长——每生成一个新策略就往里加一条约束,这意味着每轮都要重建约束集合,而不是复用。

import org.apache.commons.math3.optim.*; import org.apache.commons.math3.optim.linear.*; import java.util.ArrayList; import java.util.List; public class MarginSolver { /** 求解 max t,约束为 w·(muE - mu_i) >= t,变量顺序 [w..., t] */ public static double[] solve(double[] muExpert, List<double[]> muPolicies) { int dim = muExpert.length; double[] objCoeff = new double[dim + 1]; objCoeff[dim] = -1.0; // 单纯形默认最小化,最大化 t 要取负 List<LinearConstraint> cons = new ArrayList<>(); for (double[] muP : muPolicies) { double[] coeff = new double[dim + 1]; for (int i = 0; i < dim; i++) { coeff[i] = muExpert[i] - muP[i]; } coeff[dim] = -1.0; cons.add(new LinearConstraint(coeff, Relationship.GEQ, 0)); } // 权重范数约束的线性近似:各分量落在 [-1, 1] for (int i = 0; i < dim; i++) { double[] lo = new double[dim + 1]; lo[i] = 1.0; cons.add(new LinearConstraint(lo, Relationship.GEQ, -1)); cons.add(new LinearConstraint(lo, Relationship.LEQ, 1)); } PointValuePair sol = new SimplexSolver().optimize( new MaxIter(10000), new LinearObjectiveFunction(objCoeff, 0), new LinearConstraintSet(cons), new NonNegativeConstraint(false)); return sol.getPoint(); } }

目标函数取负号是单纯形法的标准操作,因为求解器只做最小化;MaxIter给到一万是保险值,正常问题几百次迭代就收敛,给太小会在约束集增长到几十条之后报无可行解,实际原因是迭代次数用尽而不是真的无解,这个错误信息很有迷惑性。范数约束用逐分量区间做了线性近似,这在工程上够用,代价是解可能比真正的 L2 约束更保守,权重绝对值偏小。

如果不追求最大间隔的形式,第 3 章那段梯度上升可以直接翻成 Java,代码量少一个数量级,也更容易和 Python 结果逐位对齐。我通常在需要复现论文数值时才动线性规划,做业务落地时优先用梯度版本。

4.4 与 Python 结果对齐的验证方法

跨语言重写最容易出的问题是"看起来都对,数值对不上"。有效的验证方式是切断随机性:手工构造同一份特征矩阵和同一组专家特征期望,两边跑一次梯度计算,比较输出向量的每个分量。误差在 1e-10 以内视为一致,超过 1e-6 说明中间某步的累加顺序或折扣施加位置不同。

RealVector muE = new ArrayRealVector(new double[]{-7.2, 0.5}); RealVector muP = new ArrayRealVector(new double[]{-6.1, 0.9}); RealVector g = FeatureExpectation.gradient(muE, muP); // 期望输出约 [-1.1, -0.4],与 Python 版逐分量比对 assert Math.abs(g.getEntry(0) - (-1.1)) < 1e-10; assert Math.abs(g.getEntry(1) - (-0.4)) < 1e-10;

这一步通过之后,再往上接采样和策略更新,出问题时就能确定是新加的那层引入的,而不是数值层。这个顺序别反过来,先对齐整体再查细节,中间变量一多就没法定位了。

5. 避坑与排查:示例代码里最容易翻车的五处

5.1 现象:跑通了,但学出来的权重全是零

原因通常不在求解器,在特征。如果所有特征在不同状态下的取值几乎一样,或者特征之间存在完全共线,特征期望的差值就接近零向量,梯度自然是零,LP 也会有无数个等价解而挑中最保守的那个。判断方法是打印专家特征期望向量,看各分量的量级是否在同一数量级。解决方式有两条:一是对特征做标准化,把量纲拉到同一尺度;二是删掉相关系数高于 0.95 的特征对,宁可少一个特征,也不要留一组共线的。

5.2 现象:策略在专家轨迹覆盖的状态上表现正常,换个初始状态就崩

原因是专家轨迹的覆盖不足,特征期望只在被覆盖的状态子空间上被约束住了。逆强化学习学到的是"在这批状态上怎么权衡",没见过的状态上权重外推没有依据。这个问题在纯示例代码里不会被发现,因为验证集通常和专家轨迹同分布。解决方式是在采样阶段主动加探索,让当前策略访问到专家没走过的区域,或者在特征里加入能刻画全局结构的项(比如到最近障碍的距离),而不是只依赖局部状态量。

5.3 现象:求解器报不收敛,或者解出 NaN

梯度版本里出现 NaN,九成是某一步特征期望出现了极大值把梯度撑爆了,检查折扣因子是否取到了 1.0 附近、轨迹长度是否没有上限。线性规划版本里报无可行解,先看迭代次数是不是用尽了,再看约束集里有没有互相矛盾的条目——动态加约束时如果同一个策略被重复加入两次且符号相反,就会出现不可行。解决方式是每轮加约束前做一次去重,同时给权重加一个硬性上界,防止数值发散。

5.4 现象:换机器或者从 Python 迁到 Java 之后,结果对不上

先排除随机种子,再排除累加顺序。浮点加法的结合律不成立,Python 里用 numpy 的向量化求和和 Java 里逐个累加,在特征维度上百、轨迹长度上千时,末位差异会累积到 1e-8 量级,经过几百轮迭代放大后足以让权重符号翻转。解决方式是在关键路径上统一用 Kahan 求和或者补偿求和,并且把中间结果的比对容差设到 1e-6 而不是 1e-12,后者在跨语言场景下几乎不可能满足。

5.5 现象:训练轮数越多,学到的权重反而越差

这是双层优化的典型病。外层更新步子偏大时,内层策略会被推到特征空间中完全不同的区域,此时旧的最优策略已经不代表当前奖励,梯度方向失去意义,权重开始震荡。曲线上的表现是余弦相似度先升后降。解决方式是给外层加学习率衰减,或者在内层策略更新时限制 KL 散度,保证奖励变化后策略不会跳太远。我一般会在观察到余弦相似度连续两轮下降时立刻停手,而不是等它自己回来。

6. 进阶:把中途验证做在回报曲线之前

跑到这里,代码能跑、权重方向也对,剩下的是判断"什么时候该停"。多数人盯的是策略回报曲线,但回报是四段循环的最后一段产物,等它反映出问题,前面三层已经错了很久。更早的信号是特征期望的余弦相似度:把当前策略的特征期望和专家特征期望做余弦,它衡量的是方向一致性,不受绝对量级影响,权重整体放大或缩小都不会干扰它。

我的做法是在每轮循环里加一行记录,然后看这条曲线在前 20% 轮次里的斜率:

观测到的曲线形态大概率的问题处理动作
快速上升后贴着 0.99 不动拟合正常,可以停保留当前 w,进入策略导出
上升到 0.7 附近开始横盘特征表达力不足补一两个全局特征后重跑
上下震荡无明显趋势外层步长偏大学习率减半,或加 KL 约束
一开始就下降专家特征期望算错回去检查折扣施加顺序
import numpy as np def cosine(a, b): na, nb = np.linalg.norm(a), np.linalg.norm(b) if na < 1e-12 or nb < 1e-12: return 0.0 # 零向量直接返回 0,避免除零后 NaN 污染整条曲线 return float(np.dot(a, b) / (na * nb)) # 每轮循环里记录,比只看最终权重有用得多 history.append(cosine(mu_policy, mu_expert)) if len(history) > 30 and np.mean(history[-5:]) < np.mean(history[-30:-25]): print("余弦开始回落,提前停止") break

这段判断放在循环体末尾,history保留全部记录,回看窗口取最近 5 轮和前 25 到 30 轮做均值比较,避开单轮的抖动。阈值不用调得很精细,因为我们要的是"趋势转坏"这个事实,不是精确的拐点。加了这个之后,原本要跑两千轮才能发现的发散,通常三百轮内就能拦住。

有件事我吃过亏:早期只信最终回报,一个实验跑了六个小时,最后回报确实涨了,可权重方向是反的,把奖励函数拿出来一验证才发现是特征归一化漏做,整轮作废。从那以后我每次改特征或者换求解器,都强制先跑一遍余弦相似度的短程验证,哪怕只跑两百轮,也要在动手做长实验之前把方向确认下来。这套习惯比任何调参技巧都省时间。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询