GONOGO_Qlearning改进强化学习Matlab代码实现与避坑指南 简介面向计算机、电子信息工程、数学等专业学生与强化学习初学者的GONOGO_Qlearning改进强化学习算法Matlab代码包在传统Q学习基础上优化了学习效率与稳定性适用于课程设计、期末大作业及毕业设计等实践场景。资源包共5个文件包含3个.m源码脚本、1份PDF技术文档和1张运行结果图整体压缩包大小约1.2MB结构精简便于快速定位。目前已有64人学习该代码包并下载使用。代码采用参数化编程学习率、探索率、折扣因子等关键参数均可按需调整并附赠可直接运行的案例数据帮助用户在不同任务中灵活实验注释详细、思路清晰配合技术文档与运行结果图可引导读者理解算法改进思路并快速上手二次开发。同时兼容Matlab2014至2024a等多个版本降低了环境配置门槛。1. GONOGO_Qlearning 改进强化学习 Matlab 代码它到底帮你解决什么GONOGO_Qlearning 改进强化学习 Matlab 代码这类项目在认知计算里解决的是一个非常具体的问题你手里有一批受试者做 Go/No-Go 任务的行为数据——命中率、虚报率、反应时你想用 Q-learning 解释不同人之间“该动就动、该忍就忍”的抑制能力差异。Q-learning 本身不稀奇但一落到 Go/No-Go 任务里就牵扯出动作编码、奖励设计、状态切分一连串决定每一步都直接影响模型最终能不能拟合真实行为。适合两类人一是做行为建模、想用计算模型解释反应抑制差异的研究生和课题组二是想在手头 Matlab 环境里把强化学习算法跑成可复现实验的工程师。建议先按正文把最小示例跑通再回头逐个调改进参数。2. GONOGO 任务怎么翻译成 Q-learning 状态机状态、动作与奖励矩阵2.1 Go/No-Go 在建模者眼里是个什么结构先把任务说清楚。屏幕上一个刺激出现如果是 Go 刺激你要在窗口期内尽快按键如果是 No-Go 刺激你要忍住不按。反应窗口通常在 700~1000ms。一次试次里决策者只有两个可选的离散动作按或者不按。结果也只有四种组合Go 命中、Go 漏报、No-Go 虚报、No-Go 正确抑制。从强化学习的角度看这是一个标准单步决策问题试次开始给一个状态决策者做一次选择环境随即给出奖惩试次结束没有中间状态转移。很多新手直接把教科书里的 Q-learning 更新式往里套结果跑出来的行为曲线完全不像受试者。问题通常不在算法本身而在任务翻译这一层——状态里放了什么、奖励矩阵怎么填这两个决定比学习率调参重要得多。我一般先问自己一个问题如果这个模型是真人他站在试次起点时到底知道哪些信息答案决定状态编码。模型只能感知到它那个“感知系统”允许它看到的东西。这个设定错了后面所有改进都是白搭。2.2 状态编码别让模型偷看答案最直觉的做法是把“当前刺激是 Go 还是 No-Go”直接编码成状态。比如状态 1 表示 Go状态 2 表示 No-Go两个动作对应两张 Q 表模型学到的是“Go 就按No-Go 就不按”。问题是这样模型在训练末期正确率会非常高却完全复现不了真实受试者那 10%~40% 的虚报率。因为它相当于考试时偷看了答案走的是任务规则学习通道而不是反应抑制通道。我见过不少 GONOGO_Qlearning 相关的实现真正的改进往往发生在状态编码上而不是更新公式上。一套常用的设计是把状态拆成 8 个离散值由三个二进制位拼起来当前试次类型、上一试次类型、上一试次结果是否正确。这样模型能捕获“上一把按错了这一把紧一点”这类序列效应心理学里叫错误后减慢post-error slowing是真实受试者身上非常稳定的现象。% 状态编码: 当前试次类型 上一试次类型 上一试次结果 % state_idx 4 * current_go 2 * prev_go prev_correct 1 % current_go: 1当前是Go, 0当前是NoGo % prev_go: 1上一试次是Go, 0上一试次是NoGo % prev_correct: 1上一试次选择正确, 0上一试次选择错误 function s encode_gonogo_state(trial_types, results, t) if t 1 prev_go 0; % 第一个试次没有历史, 默认按NoGo处理 prev_correct 1; % 且默认上一把正确, 不让模型一开始就有负罪感 else prev_go double(trial_types(t-1) go); prev_correct double(results(t-1) 0); end current_go double(trial_types(t) go); s 4 * current_go 2 * prev_go prev_correct 1; end这段编码的逻辑是把“当前刺激类型”放在最高位权重最大上一试次类型次之上一试次对错放最低位。改动这个排列会直接影响 Q 表的学习顺序因为 Q-learning 是逐试次更新的高位决定状态之间的最大距离。三个 bit 全部用上就是 8 个状态每个状态两个动作Q 表是 8×2 的矩阵。如果把当前刺激类型这个 bit 拿掉模型就只能靠历史信息猜测此时它会退化成一个先验驱动模型虚报率偏高但能复现“冲动控制失败”的特征。到底用哪种编码取决于你的研究问题是“任务学习”还是“抑制控制”。前者留当前位后者隐去当前位。这两个版本我都跑过结论是不先做参数恢复就选型基本等于开盲盒。2.3 动作与奖励矩阵的数值设计动作空间简单就是按与不按。真正的旋钮在奖励矩阵。很多人在 Go/No-Go 任务里沿用经典 Q-learning 的稀疏奖励Go 命中给 1其他情况给 0。这样模型很快发现“什么都不按”在某些试次能拿 0而按错了会拿负值于是策略坍缩成“全程发呆”。我给出一套在行为拟合里跑得比较稳的不对称奖励矩阵参数含义和默认值如下表试次类型动作按动作不按Go1命中-0.3漏报No-Go-1虚报0.8正确抑制每一格都不是拍脑袋。漏报设 -0.3 而不是 0是为了让“无脑按”和“无脑不按”两个退化策略的期望收益都不占优。假设 Go 与 No-Go 各占一半“无脑按”的期望是 0.5×1 0.5×(-1)0“无脑不按”的期望是 0.5×(-0.3)0.5×0.80.25比前者略高但明显低于正确策略如果漏报设 0那“无脑不按”的期望变成 0.4模型会更倾向于偏向不按虚报率被压得过低拟合真实受试者时总是对不上。正确抑制给 0.8 而不是 1是我压低了它真实受试者对“忍住没按”的主观价值通常低于“命中目标”的奖励感。这个 0.8 与 1 的差距就是速度-准确性权衡的量化旋钮。奖励差越大模型越激进差越小模型越保守。想做个体差异建模优先调的就是这两个值的比值。3. 改进 Q-learning 的三个关键旋钮奖励塑形、温度退火与历史状态3.1 教科书更新式为什么在 GONOGO 上欠拟合常规 Q-learning 更新式是 Q(s,a) ← Q(s,a) α[r γ·max Q(s,a) - Q(s,a)]。这个式子本身没有错但放到 Go/No-Go 的单步场景里γ 那一项就出了问题。单步任务的试次与试次之间没有状态转移下一个状态是由实验设计随机决定的不是由当前动作决定的。γ0 等于把未来试次的平均收益折现到当前状态里训练后期 Q 值会出现高方差抖动。我在 GONOGO 这类任务里直接把 γ 设成 0。改进强化学习的重心也从改更新公式转移到了改任务翻译奖励怎么塑形、探索温度怎么退火、状态里塞不塞历史信息。这三个点决定模型是“学会规则”还是“学会抑制”比 α 和 γ 加起来都重要。3.2 改进点一不对称奖励塑形对应 gonogo_reward.m奖励塑形是这套代码里最值得动手改的地方。上一章那张表落地成函数就是下面这样。注意参数全部放在 params 结构体里方便网格搜索。function r gonogo_reward(trial_type, action, params) % trial_type: go 或 nogo % action: 1按, 2不按 % params 字段: % reward_go Go命中奖励, 默认 1 % penalty_fa NoGo虚报惩罚, 默认 -1 % reward_nogo NoGo正确抑制奖励, 默认 0.8 % penalty_miss Go漏报惩罚, 默认 -0.3 switch trial_type case go if action 1 r params.reward_go; % 命中 else r params.penalty_miss; % 漏报 end case nogo if action 1 r params.penalty_fa; % 虚报: 惩罚最重 else r params.reward_nogo; % 正确抑制 end end end这个函数的四个返回值里有两个关系最敏感。reward_nogo / reward_go的比值控制模型的保守程度比值越接近 1模型越倾向于不按penalty_fa - penalty_miss的差值控制模型对惩罚的敏感度差值越大模型越容易在犯错后大幅调整策略。调参时先固定奖励矩阵只动学习率和温度奖励矩阵放最后微调因为它是行为拟合的最顶层杠杆动一下全局都变。3.3 改进点二softmax 温度退火与探索预算Go/No-Go 这种双动作任务如果直接用贪心策略模型一旦在早期试次里偶然发现“不按”能拿到正奖励就会锁定这个动作后面再也翻不了身。这是探索不足的典型症状。我一般用 softmax 选择策略温度 τ 控制随机性τ 大动作选择接近均匀分布τ 小接近贪心。function [action, prob] softmax_policy(Q, s, tau) % softmax 策略, tau 是温度参数 % tau 建议初始值 0.5~1.0, 每 50 试次衰减一次(乘 0.98) % tau 必须设下限, 我常用 0.2, 防止模型彻底锁死 logits Q(s, :) / tau; logits logits - max(logits); % 数值稳定, 防止 exp 溢出 exp_logits exp(logits); prob exp_logits / sum(exp_logits); action randsample(1:2, 1, true, prob); end注意logits - max(logits)那一步。Q 值如果累计到十几除以一个很小的 τ 后 exp 会溢出成 inf加这一步把所有 logits 平移到以最大值为 0概率结果不变但数值上永远不会炸。温度的衰减策略是另一个坑。衰减太快探索预算用尽模型同样会锁死衰减太慢后期一直在乱选收敛不稳定。我的习惯是每 50 个试次乘 0.98同时设下限 0.2。1000 个试次跑下来τ 从 1.0 降到 0.2 左右前期有足够的探索空间后期也能保持一点随机性去模拟真实受试者那 10%~40% 的虚报——真实人类本来就不是贪心决策者。3.4 改进点三扩展状态与序列依赖第 2.2 节的 8 状态编码本身就是改进的一部分这里展开它的行为学动机。真实受试者在 Go/No-Go 任务里表现出明显的序列依赖上一个试次如果是 No-Go 且按错了下一个试次的反应时会明显变慢虚报率下降。用心理物理学的说法这是“错误后调整机制”。如果状态编码里不包含上一试次的信息模型只能用随机噪音去吸收这部分方差拟合优度天然受限。把三个 bit 全用上之后模型就具备了“记住上一把”的能力。比如状态 5当前 Go、上一把 NoGo、上一把正确和状态 1当前 Go、上一把 NoGo、上一把错误的 Q 值差异就反映了错误后调整的幅度。你可以直接打印这两个状态下的 Q 值看模型有没有学会“犯过错之后更谨慎”。这个思路推而广之跟很多控制类强化学习项目的状态设计逻辑一致凡是你认为会影响决策的变量都应该出现在状态里放不进去的变量模型就只能用随机性去拟合。设计状态时宁可多一个 bit也不要让模型在黑匣子里替你决定什么重要。4. 在 Matlab 里跑通完整训练流程文件组织、主脚本与可视化4.1 文件组织与最小复现清单拿到手解压之后我的习惯是不管原目录长什么样先按下面这套结构整理一遍再动代码。每个文件的职责单一排查问题时能少翻好几次文件。GONOGO_Qlearning/ |-- main_gonogo.m % 主脚本: 生成试次, 训练, 调画图 |-- encode_gonogo_state.m % 状态编码函数 (第2章) |-- gonogo_reward.m % 奖励函数 (第3章) |-- softmax_policy.m % softmax 策略 (第3章) |-- gonogo_qlearning_update.m % 单步 Q-learning 更新 |-- plot_results.m % 画学习曲线与 Q 值轨迹这里最关键的是主脚本里第一行的随机种子。Go/No-Go 试次序列是随机生成的不固定种子的话每次跑出来的行为曲线都不一样你根本分不清模型效果的差异是来自算法改进还是来自随机序列。我固定用rng(42)这是个小到自己都快忘了的习惯但每次复盘时它都是救命的那一条。4.2 主脚本生成试次、在线更新、记录指标主脚本的任务是串联前面几个函数。下面这个版本跑的是一个简化流程70% Go、30% No-Go共 1000 个试次在线更新 Q 表实时记录命中率和虚报率。%% 主脚本: 在线学习 行为指标记录 rng(42); % 固定随机种子, 保证结果可复现 n_trials 1000; n_go 700; n_nogo 300; trial_types [repmat(go, n_go, 1); repmat(nogo, n_nogo, 1)]; trial_types trial_types(randperm(n_trials)); % 打乱顺序, 防止连续同类刺激 Q zeros(8, 2); % 8状态 x 2动作 alpha 0.1; % 学习率 gamma 0; % 单步任务不用折扣 tau 1.0; % softmax温度初始值 params struct(reward_go, 1, penalty_fa, -1, ... reward_nogo, 0.8, penalty_miss, -0.3); results zeros(n_trials, 1); % 记录每试次奖励 hit zeros(n_trials, 1); % Go命中标记 fa zeros(n_trials, 1); % NoGo虚报标记 for t 1:n_trials s encode_gonogo_state(trial_types, results, t); [a, ~] softmax_policy(Q, s, tau); if trial_types(t) go a 1 hit(t) 1; elseif trial_types(t) nogo a 1 fa(t) 1; end r gonogo_reward(trial_types(t), a, params); Q gonogo_qlearning_update(Q, s, a, r, alpha, gamma); results(t) r; if mod(t, 50) 0 tau max(0.2, tau * 0.98); % 温度退火 end endencode_gonogo_state的返回值范围是 1~8正好落在 Q 表行索引内。gonogo_qlearning_update在第 3 章已经给过内部就是单步差分更新γ 传 0直接把max Q(s,:)那一项省掉。跑完这段hit和fa里存的是逐试次的正确/错误标记results存的是奖励值三组变量后面画图都要用。我建议第一次跑的时候把alpha从 0.1 改成 0.3 看一眼效果学习率对收敛速度的影响在这个任务里非常直观改大之后 Q 值会大幅震荡学习曲线锯齿感明显改小到 0.03 则收敛变慢1000 个试次可能不够用。这个对比做完你对 Q-learning 的 α 就不再只是“知道公式”了。4.3 可视化学习曲线与 Q 值轨迹代码能跑通不代表模型学对了一定要画图看。我的标准配置是两张图一张是命中率和虚报率的滑动平均曲线另一张是关键状态下的 Q 值轨迹。滑动窗口取 50 个试次这是行为建模里比较通用的默认值窗口太长会抹掉早期学习阶段的细节太短则看不出趋势。% plot_results.m: 画学习曲线与Q值轨迹 window 50; hit_smooth movmean(hit, window); fa_smooth movmean(fa, window); figure; plot(1:n_trials, hit_smooth, g-, LineWidth, 1.5); hold on; plot(1:n_trials, fa_smooth, r-, LineWidth, 1.5); legend({Go命中率, NoGo虚报率}); xlabel(试次); ylabel(比例); grid on;判断模型是否正常我只看一个核心指标Go 命中率是否稳定在 0.8 以上同时 No-Go 虚报率是否落在 0.1~0.4 之间。如果虚报率趋近于 0说明模型太“完美”奖励矩阵里正确抑制的奖励给高了如果虚报率一直在 0.5 附近晃说明模型没学会区分两个刺激类型问题大概率出在状态编码上。Q 值轨迹图也很有价值。挑两个有意思的状态比如“当前 Go、上一把 NoGo、上一把错误”和“当前 Go、上一把 NoGo、上一把正确”把两个动作的 Q 值都画出来。前者 Q 值如果明显低于后者说明模型真的学到了错误后调整。这一步经常能发现“模型在作弊但指标很好看”的情况——只有 Q 值轨迹能暴露内部策略。5. GONOGO_Qlearning 的避坑清单五个翻车现场与排查办法5.1 现象No-Go 虚报率一直接近 50%死活降不下来模型跑完 1000 个试次虚报率还在 45%~50% 晃看起来像完全没有学习。原因奖励矩阵对称。Go 命中 1、No-Go 虚报 -1同时 No-Go 正确抑制 1、Go 漏报 -1这样“全按”策略和“全不按”策略在 Go/No-Go 各半时期望收益相同模型找不到梯度方向策略在两个动作之间随机漂移。解决把奖励矩阵改成不对称结构我在第 2.3 节给的那组数可以直接用。关键是让两个退化策略的期望收益都不占优最优策略只剩一个。改完之后虚报率通常在 200 个试次内就会落到合理区间。注意改奖励矩阵时要同步检查温度下限温度过低会让模型锁死在前期偶然发现的次优策略上。5.2 现象Q 值爆炸跑到几十上百训练到一半Q 表里出现几十几百的数值softmax 概率变成 0/1 两极分化。原因奖励数值设太大比如命中给 10、虚报给 -10再配合固定的学习率早期试次的单次大奖励就把 Q 值推得非常高。Q-learning 的单步更新对奖励幅度非常敏感不像深度强化学习那样有专门的 reward scaling 过程。解决把奖励值限定在 [-1, 1] 区间。我见过有人用 5/-5 也跑得动但行为拟合时参数解释性会变差因为奖励大小直接影响 Q 值语义不方便跨模型对比。奖励塑形阶段先统一尺度后续做个体拟合时再放大缩小这样网格搜索的步长才好设。5.3 现象温度衰减太快模型后半程翻不了身前期 200 个试次里模型学会了“不按”后面把 Go 试次比例加到 80%命中率还是上不去。原因τ 按固定衰减率一直降到接近 0探索预算耗尽模型行为变成了纯贪心。前期如果 No-Go 试次集中出现模型先学会了“不按拿正奖励”τ 降到 0.2 以下后Go 试次的负反馈已经推不动 Q 值改变。解决给 τ 设下限我用的下限是 0.2。更激进的做法是分段退火前 500 试次 τ 从 1.0 线性降到 0.5后 500 试次从 0.5 降到 0.2。这样后期仍然保留随机选择的空间不仅能模拟真实受试者的非理性虚报还能防止模型策略锁死。这个调整在行为拟合里基本是刚需不加下限的模型几乎都会在后期出现某种形式的僵化。5.4 现象仿真数据上正确率 100%真实受试者数据拟合优度极差模型在人工生成的干净数据上表现完美换到真实行为数据就崩。原因状态泄漏。如果状态编码里直接放了“当前试次类型”这个 bit模型相当于每把都偷看了答案学的是任务规则本身而不是反应抑制的决策过程。真实受试者看不到答案他们是在不确定性下做判断模型却在一个确定性世界里学习两者根本不是同一个问题。解决把“当前试次类型”从状态里去掉或者改用第 2.2 节提到的“纯历史状态”编码。验证方法是做参数恢复第 6 章会详细讲如果模型从已知参数仿真数据里恢复不出原参数说明学习过程本身有作弊通道。这个是所有坑里最隐蔽的Q 值表漂亮、学习曲线平滑全是假象。5.5 现象一组参数拟合不了整批受试者把 α0.1、τ1.0 套给所有受试者发现有的人拟合得很好有的人误差大得离谱。原因个体差异。有的受试者对奖励敏感有的对惩罚敏感有的探索性强统一的固定参数等于假设全组人同一个决策风格这在行为数据里几乎不可能成立。解决逐受试者拟合。用网格搜索或 fminsearch 分别估计每个人的 α 和 τ然后再对参数分布做群体统计。网格搜索的粒度我一般用 α ∈ {0.03, 0.05, 0.1, 0.2}τ ∈ {0.3, 0.5, 0.8, 1.2}每个组合跑 500 试次求平均负对数似然选最优组合。这一步跑完你得到的就不只是“模型拟合效果还行”而是能说出“高虚报率受试者的 τ 显著更高”这样的可解释结论。6. 验证模型没白做参数恢复与跨环境对照6.1 用参数恢复确认实现没有 bug我的规矩是任何新模型跑到真实数据上之前先做一遍参数恢复。做法是先用已知参数比如 α0.1, τ0.8仿真生成 100 个“虚拟受试者”数据集再用最大似然拟合去反推每个数据集的参数最后画真实参数与恢复参数的散点图。如果恢复出来的参数乱七八糟说明实现里有 bug 或者奖励函数设计不对这时候千万别上真实数据否则你拟合出来的参数毫无意义。function nll gonogo_nll(theta, data) % theta [alpha, tau] % data: 结构体数组, 每个元素含 state, action, reward Q zeros(8, 2); alpha theta(1); tau theta(2); nll 0; for t 1:numel(data) s data(t).state; a data(t).action; logits Q(s, :) / tau; logits logits - max(logits); p exp(logits) / sum(exp(logits)); nll nll - log(p(a)); % 负对数似然 Q gonogo_qlearning_update(Q, s, a, data(t).reward, alpha, 0); end end拟合时用 fminsearch 最小化这个函数就行。恢复出的参数与真实值的相关系数 R² 到 0.9 以上说明模型可辨识低于这个数优先查状态编码和奖励矩阵。这一步能省下后面好几周的无效拟合时间。6.2 一个跨环境检查习惯如果你对 Matlab 实现没底可以把同一套 Q-learning 更新式放到 Python gymnasium 的 CartPole 或一个简单的 bandit 环境里跑一遍对比等效超参下的行为曲线。CartPole 这种环境的示例代码讲解很多把你的奖励塑形函数平移到那边做对照能快速定位问题是出在更新公式还是出在 GONOGO 的任务翻译层。两个环境的行为趋势一致再回 Matlab 继续调参。我自己踩过最大的坑就是状态泄漏那一次学习曲线漂亮得像见鬼后来做参数恢复才发现模型根本没在学抑制。这之后我的习惯就固定成先参数恢复再碰真实数据。这段弯路走下来至少能帮你省掉一轮焦虑的调参。希望帮到你。本文还有配套的精品资源点击获取