ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

GONOGO任务与Q-learning结合的Matlab强化学习建模指南

GONOGO任务与Q-learning结合的Matlab强化学习建模指南 简介GONOGO_Qlearning改进强化学习Matlab代码是一套面向计算机、电子信息工程、数学等专业大学生的强化学习算法实现资源适用于课程设计、期末大作业和毕业设计等真实动手环节。代码采用参数化编程学习率、探索率、折扣因子等关键参数均可方便调整同时配有详细注释和可直接运行的案例数据支持Matlab 2014、2019a与2024a环境。压缩包共5个文件其中3个m脚本分别实现算法主体、动作计算与运行统计1份PDF文档系统讲解Q学习原理1张jpg图直观展示运行结果整个压缩包仅1.2MB轻量易部署。这份代码已有64人学习适合希望快速上手改进强化学习算法的学生与研究者。通过阅读源码并对照结果图读者能直观理解GONOGO_Qlearning的迭代更新机制并在此基础上扩展属于自己的仿真实验或课程项目。1. GONOGO任务遇上Q-learning这套Matlab代码在解决什么问题跑完一轮Go/No-Go行为实验手头攒了几百个试次的反应时和按键记录最想知道的通常不是“平均正确率是多少”而是“被试到底是怎么学会在Go刺激下按键、在No-Go刺激下忍住不按的”。传统的E-Prime导出数据用SPSS做重复测量方差分析只能回答“学会了没有、两组有没有差异”回答不了“学习过程如何展开、错误反馈如何修正内部决策”。GONOGO_Qlearning改进强化学习Matlab代码这个方向就是把Q-learning当成一个逐试次的决策模型让Q表在仿真环境里跟着同一份试次序列走用Q值变化还原被试的决策学习轨迹。它适合认知建模方向的研究者、用行为数据做计算分析的工程师也适合拿Matlab练手强化学习的学生。难点不在Q-learning本身而在GONOGO任务里两类错误代价不对称这件事该怎么写进算法。2. GONOGO任务里的强化学习建模状态、动作与奖励的三要素设计2.1 Go/No-Go范式为什么适合用Q-learning描述Go/No-Go任务从计算角度看是一个很干净的序贯决策问题每个试次呈现一个刺激被试只能做两个动作之一按键或不按键随后立即收到反馈。这正好是一个表格型马尔可夫决策过程MDP状态是刺激类型动作是行为选择奖励是反馈信号。Q-learning的价值在于它不要求我们知道环境转移概率只需要逐试次观察“状态-动作-奖励-下一代状态”这个四元组就能逐步逼近最优动作值函数。David Silver的强化学习课程前五讲里讲得最透的就是Q-learning的收敛逻辑Q表更新实际上是在做不动点迭代只要每个状态-动作对被无限次访问、学习率满足Robbins-Monro条件Q值就会以概率1收敛到最优值函数。这一点在GONOGO场景里特别受用因为实验试次数通常只有几百状态空间只有2到4个完全在表格型Q-learning的能力范围内。有同行常问现在深度强化学习这么热为什么不直接上DQN我的观点是如果你的状态特征是one-hot刺激类型而不是高维图像用神经网络反而引入大量不必要的参数和退化风险。DQN的回放缓冲区、目标网络、梯度裁剪这一整套机制在状态维度低于10的GONOGO问题里完全派不上用场还让结果难解释。表格型Q-learning五个参数就能说清模型行为这对一篇行为实验论文的计算模型部分来说是极大的优势。2.2 状态特征怎么定刺激类型、线索与历史信息最简状态下状态就是刺激类型Go刺激记为状态1No-Go刺激记为状态2这是绝大多数GONOGO建模论文的起点。但如果你拿这个最简状态去拟合真实数据通常会差一口气。原因是人的决策不只依赖当前刺激还受上一试次的结果和刺激序列结构影响这个现象叫序列效应sequential effect在认知心理学里已经被反复验证前一个试次是Go且按对了当前试次按Go的倾向会上升前一个试次是No-Go且按错了当前试次会变得更保守。所以我会把状态扩展成几个变体。第一个变体是“刺激类型上一试次结果”状态数从2变成4Go正确、Go错误、No-Go正确、No-Go错误。第二个变体是“刺激类型上一试次刺激类型上一试次反应”状态数变成8对100到200个试次的数据来说8个状态每个平均被访问12到25次Q表仍然能可靠收敛。第三个变体是把累计错误次数加入状态这个对建模疲劳效应有用但状态数会进一步膨胀我一般只在试次数超过400时才启用。这里有个取舍要讲清楚状态越细模型对行为数据的解释力越强但每个状态被访问的次数越少Q估计的方差越大。状态数并不是越多越好第5章的避坑部分会专门展开这个。2.3 奖励函数的不对称设计Go错误与No-Go错误的代价差GONOGO任务里两类错误在认知机制上是不同的。Go错误在No-Go刺激下按了键是抑制失败反映的是动作抑制回路没拦住运动冲动No-Go错误在Go刺激下没按键是注意或动机缺失反映的是目标检测环节掉了链子。实验心理学里一般会分别统计这两类错误率因为它们的神经机制和药物影响完全不同。因此模型里的奖励函数也必须不对称常见做法是下面这张奖励矩阵状态动作Go动作No-GoGo刺激1.0-0.3No-Go刺激-1.00.5Go刺激下按对是标准奖励1No-Go刺激下忍住不按给0.5因为抑制成功比简单按键更费劲No-Go刺激下按错给-1.0压过Go刺激下的正确奖励Go刺激下漏按给-0.3轻微惩罚即可。这个设计的核心是满足一个不等式在No-Go刺激下Go动作的期望回报必须显著低于No-Go动作且差距要大于Go刺激下两个动作的差距。换句话说模型必须学到一个强不对称性才能复现真实被试那种“Go错误率低、No-Go错误率略高”的模式。直接把两个动作的奖励设成对称1/-1模型学出来往往会变成“永远选Go”的退化解因为Go动作在状态1下的收益太高了——这个问题在改进Q-learning时非常典型第5章会把它当第一个踩坑案例展开。3. 标准Q-learning的Matlab最小实现先让Q表在GONOGO上学起来3.1 从零写Q表更新核心代码与参数先别管改进把标准Q-learning在Matlab里的最小实现跑通。核心就三行选动作、算TD误差、更新Q表。下面的代码是完整可跑的从一个2状态2动作的Q表开始。% 参数设置 alpha 0.1; % 学习率决定新信息覆盖旧估计的幅度 gamma 0.9; % 折扣因子越大越看重长期回报 epsilon 0.3; % 初始探索率30%的试次随机选动作 epsilon_min 0.05; % 探索率下限防止后期完全贪婪 decay 0.99; % 每试次探索率衰减系数 % Q表初始化2个状态 x 2个动作全0启动 Q zeros(2, 2); % 单个试次的Q-learning更新主体 % s: 当前状态(1Go刺激, 2No-Go刺激) % a: 当前动作(1Go按键, 2No-Go不按) % r: 反馈奖励 % s_next: 下一个状态GONOGO里独立试次通常等于当前状态 % 选择动作epsilon-greedy if rand epsilon a randi(2); % 探索随机选 else [~, a] max(Q(s, :)); % 利用选Q值最大的 end % 更新Q值 td_error r gamma * max(Q(s_next, :)) - Q(s, a); Q(s, a) Q(s, a) alpha * td_error;这段代码里最关键的是td_error那一行。max(Q(s_next, :))是下一个状态下最优动作的估计回报gamma把它折现后加上实际奖励r再减去当前Q(s,a)差值就是TD误差。Q-learning的off-policy特性就在这里即使实际执行的是随机探索动作更新时也假设未来按最优策略走这让它能直接逼近最优Q值而不是behavior policy的Q值。alpha建议取0.1到0.5之间。GONOGO试次数少alpha太大会震荡太小则收敛慢。gamma取0.9比较合理因为GONOGO每个试次独立长期回报其实不是真正的“长期”gamma在0.8到0.95之间都说得过去。epsilon初始值0.3配合decay0.99在100个试次后探索率降到0.3乘以0.99的100次方约等于0.11300个试次后降到0.05下限附近——这个衰减节奏和真实被试“前几十个试次乱按、后面稳定”的行为模式很接近。3.2 epsilon-greedy策略探索率怎么衰减才不翻车epsilon-greedy有两个极端做法都会翻车。第一个是epsilon恒定为0.5不衰减模型永远有50%的试次在随机按最终正确率天花板只有75%50%的探索试次里对一半50%的开发试次里全对。第二个是一上来就把epsilon设为0.01模型几乎没有探索机会前几个错误反馈就把Q表钉死在初始偏好上陷入局部最优。我常用的衰减策略是分段衰减而不是指数衰减前20个试次用0.4的固定探索率模拟“被试还没读懂规则”第21到第80个试次线性降到0.1之后固定0.05。这样模型行为曲线会呈现明显的“早期乱、中期快速学、后期进入平台期”三段形态和真实被试的学习曲线形状对得上。当然如果你的实验对象是训练充分的猴子或成年人类初始探索率可以调低到0.15。实现分段衰减也不复杂在循环里加两个判断就行% 分段探索率衰减 if t 20 epsilon 0.4; elseif t 80 epsilon 0.4 - 0.3 * (t - 20) / 60; % 线性降到0.1 else epsilon 0.05; end分段衰减的好处是参数语义清晰前20个试次对应实验里的“规则探索阶段”中间60个试次对应“快速学习阶段”之后是“稳定表现阶段”。将来拟合真实数据时这三个阶段的边界本身也可以作为自由参数估计用来检验被试的学习节奏。3.3 跑一个100试次的仿真完整脚本与逐行说明把上面的散装代码整合成一个完整仿真才能看整体行为。下面这段脚本可以直接跑输出每个试次的正确率滑动平均。% GONOGO 标准Q-learning仿真完整 clear; clc; rng(42); % 固定随机种子保证可复现 % 1. 参数区 alpha 0.1; gamma 0.9; epsilon 0.4; epsilon_min 0.05; decay 0.98; n_trials 100; n_states 2; n_actions 2; % 2. 奖励矩阵 R(s,a)状态1Go刺激状态2NoGo刺激 R [1.0, -0.3; -1.0, 0.5]; % 3. 试次序列50% Go、50% NoGo伪随机打乱 trial_types [ones(1, n_trials/2), 2*ones(1, n_trials/2)]; trial_types trial_types(randperm(n_trials)); % 4. 初始化 Q zeros(n_states, n_actions); acc zeros(1, n_trials); % 记录每试次对错 % 5. 主循环 for t 1:n_trials s trial_types(t); % 选动作 if rand epsilon a randi(n_actions); else [~, a] max(Q(s, :)); end % 给奖励按矩阵取值 r R(s, a); % GONOGO是独立试次下一状态当前状态 s_next s; % Q更新 td_error r gamma * max(Q(s_next, :)) - Q(s, a); Q(s, a) Q(s, a) alpha * td_error; % 记录动作对则acc(t)1 acc(t) (a s); % 探索率衰减 epsilon max(epsilon * decay, epsilon_min); end % 6. 画20试次滑动平均正确率 figure; hold on; window 20; smooth_acc movmean(acc, window); plot(smooth_acc, b-, LineWidth, 1.5); yline(0.5, k--); xlabel(试次); ylabel(正确率20试次滑动平均); title(标准Q-learning在GONOGO上的学习曲线); ylim([0 1]);运行这段代码你会看到正确率从初始0.5附近快速爬升大约在第40到第60个试次后贴近0.9以上。注意几个细节rng(42)固定了随机种子保证每次跑出来的曲线完全一致movmean是R2016a以后引入的函数如果你用的是旧版本需要自己写一个滑动平均循环第5章的兼容性排查会讲到。奖励矩阵R里Go刺激下No-Go动作的-0.3以及No-Go刺激下Go动作的-1.0这两项是整个模型行为走向的关键——把它们改成对称1/-1你会看到模型倾向于把所有试次都按Go。4. Qlearning改进的四个落地方向不对称奖励、双Q、资格迹与状态扩展4.1 不对称奖励矩阵把错误代价写进公式标准Q-learning的奖励函数是环境给的很多入门实现直接写成“对了1错了-1”这在GONOGO任务里是有问题的。前面2.3节已经提过不对称奖励矩阵的长相这里重点说怎么把它真正写进更新公式。做法很简单奖励矩阵R直接按实验设计的错误代价填。Go正确1、Go漏按-0.3、NoGo误按-1.0、NoGo正确0.5。效果是模型学到的稳态策略会倾向于“Go刺激下按键NoGo刺激下忍住”而不是“全都按键”。这里有个值得注意的点奖励矩阵本身也是模型参数不是固定不变的。拟合真实行为数据时可以把R里的数值当作自由参数来估计。常见基线是Go正确奖励固定为1作为尺度锚点NoGo正确奖励设为pGo漏按设为-q1NoGo误按设为-q2。拟合的目标是让模型仿真出来的两类错误率与实际被试数据最接近。我用fmincon做这个参数拟合时发现q2通常在0.8到1.2之间而q1往往只有0.2到0.4这个不对称在几乎每个被试上都显著——它对应的是认知控制研究中“抑制失败对行为的影响比注意缺失更大”的经典结论。4.2 Double Q-learning消除最大化偏差标准Q-learning用同一个Q表既选动作又估值max操作会把估计误差放大这叫最大化偏差maximization bias。在GONOGO这种奖励不对称的场景里偏差表现为状态1Go刺激下两个动作的Q估计都带噪声max取的是偏大的那个于是某个动作被高估模型就反复选它即使真实期望不是最高的。在NoGo刺激下Go动作的负奖励也可能被高估到接近0导致抑制失败的试次比预期多。Double Q-learning的做法是同时维护两张Q表Q1和Q2更新时用Q1选动作、用Q2估值两个角色交替。Matlab实现也就多几行% 初始化两张Q表 Q1 zeros(n_states, n_actions); Q2 zeros(n_states, n_actions); % 在每试次更新中随机决定更新哪张表 if rand 0.5 [~, a_star] max(Q1(s_next, :)); Q1(s, a) Q1(s, a) alpha * (r gamma * Q2(s_next, a_star) - Q1(s, a)); else [~, a_star] max(Q2(s_next, :)); Q2(s, a) Q2(s, a) alpha * (r gamma * Q1(s_next, a_star) - Q2(s, a)); end % 使用时取两张表的均值 Q_mean (Q1 Q2) / 2;关键在第4行和第6行Q1的更新用的是Q2(s_next, a_star)来估值其中a_star根据Q1选出——选动作和估值分离偏差就不会自举放大。仿真结果上Double Q-learning在试次数较少50到100试次时优势明显标准Q-learning的Go错误率可能收敛到10%到15%Double Q-learning能压到5%以下。代价是要多维护一张Q表计算量翻倍但对表格型问题来说几乎可以忽略。注意试次数少于200时两张Q表各自的访问次数会减半估计方差反而可能变大。这个“什么时候该用双Q”的判断在第5章还会细说。4.3 Eligibility Trace让历史状态也吃到延迟奖励GONOGO每个试次独立理论上不存在跨试次的延迟奖励但如果你把序列效应加进来——上一试次的错误会影响本试次的决策——那就有了跨时间的依赖。这时标准Q-learning的短板就暴露了TD(0)只更新当前状态的Q值如果奖励延迟了若干步信息要一步一步往回传效率很低。Eligibility Trace资格迹的思路是维护一个痕迹表E(s,a)每次访问某个状态-动作对就给E加1之后每步按gamma乘以lambda衰减。更新时不仅Q(s,a)要更新所有E大于0的状态-动作对都要按E加权更新。Matlab核心代码就多三行% E表初始化 E zeros(n_states, n_actions); % 每步更新E表 E(s, a) E(s, a) 1; % 对全部状态-动作对做Q更新 delta r gamma * max(Q(s_next, :)) - Q(s, a); Q Q alpha * delta * E; % 每步衰减E表 E gamma * lambda * E;lambda取0.3到0.7之间取值越大历史状态分到的更新越多收敛越快但方差也越大。对GONOGO来说lambda取0.3到0.5就够了因为跨试次依赖本来就弱。注意E表叠加前要裁剪防止某个状态-动作对被反复访问导致E值爆炸——这是踩过坑的地方。实际项目里我通常会加一行E min(E, 5); 来限幅。4.4 状态扩展把反应时和上一试次结果纳入特征状态扩展不是新算法但它是提升模型拟合度性价比最高的手段。前面2.2节列过三个变体这里给出第二个变体的Matlab实现思路状态编码成一个索引值代码层面用一个映射函数。% 状态编码函数 % 输入: stim(当前刺激1/2), prev_resp(上一试次实际动作1/2) % 输出: state_index(1~4) function idx encode_state(stim, prev_resp) idx (stim - 1) * 2 prev_resp; end % 主循环里的状态获取 s encode_state(trial_types(t), prev_actions); % 更新完动作后记录 prev_actions a;状态数从2扩到4每个状态仍然有充足的访问次数。再往上扩到“刺激上一试次结果上一试次动作”状态数就到8了。我在拟合真实数据时发现8状态模型相比4状态模型的拟合优度提升不显著但4状态相比2状态提升非常明显。换句话说对大部分GONOGO数据状态数2到4就够了8是个上限。更激进的做法是把因果推断工具嵌入强化学习流程——也就是因果强化学习CRL的思路——把“上一试次是否错误”当作一个干预变量估计它对当前动作选择的因果效应。但对GONOGO这种状态维度个位数的场景CRL能带来的增量很有限反而引入了额外的估计方差。我建议先把状态扩展和不对称奖励做好剩下再考虑其他机制。如果你的数据是历史积累的离线决策日志而不是在线逐试次学习那考察的是另一条路——IQL离线强化学习它专门处理无法与环境在线交互的情况。GONOGO实验本质上是在线学习IQL不太适用但值得知道有这条技术路线存在。5. 避坑排查Q值发散、永远选Go、曲线震荡的检查清单GONOGO建模的坑大多集中在奖励设计、探索率、状态访问次数和版本兼容上。下面5条是过去一年里被问得最多的问题每条按“现象→原因→解决”写。5.1 现象Q值全部冲到正无穷几百个试次后Q值变成10的6次方级别的正数学习曲线和正确率曲线都随之失控。原因几乎总是奖励函数没有归一化或alpha太大。GONOGO奖励撑死就正负1Q值稳态应该在-3到3区间内如果发现Q值超过10说明学习率过高alpha大于0.5导致TD误差被反复放大或者奖励里带了正的大常数让每步更新都在正向累积。解决把奖励区间压到-1到1alpha降到0.1到0.2gamma不超过0.95。排查的时候可以先跑一个固定策略全Go或全NoGo的轨迹确认Q值有界再放开epsilon-greedy。也可以在循环里加一道保险% 排查用打印Q值范围 if any(abs(Q(:)) 10) warning(Q值超出正常范围检查奖励矩阵和学习率); disp(R); disp([alpha, gamma]); end5.2 现象模型永远选GoNo-Go正确率是0这是GONOGO建模最经典的翻车现场。模型把所有试次都按GoNoGo刺激下永远错。原因在于奖励矩阵里Go刺激下Go动作的1与NoGo刺激下Go动作的-1互相拉扯如果后期epsilon降到0.01以下模型一旦在Go刺激下被1强化就可能把所有状态都映射成Go动作。特别是如果gamma取0.991奖励会被折现回传进一步强化这个坏策略。解决分两步。第一步确认奖励矩阵确实不对称且NoGo误按惩罚绝对值大于等于Go正确奖励即R(2,1)的绝对值大于等于R(1,1)。第二步检查epsilon是否衰减过快如果前20个试次里NoGo刺激恰好出现得少模型根本没建立NoGo状态的正确Q值就进入了低探索期。我一般会在主循环里加一行验证代码打印每个状态被访问的次数% 打印每个状态被访问的次数确保两个状态都不少于总试次的20% state_count zeros(1, n_states); for t 1:n_trials state_count(trial_types(t)) state_count(trial_types(t)) 1; end disp(state_count);如果发现某个状态访问次数太少最简单的办法是改用固定顺序的ABBA平衡设计而不是完全随机打乱保证两种刺激在前20个试次里都出现足够多次。5.3 现象学习曲线震荡不下降正确率曲线在0.5到0.8之间反复横跳20试次滑动平均也不见上升趋势。最常见的原因是gamma取太高0.99把跨试次的随机方差当成长期信号。GONOGO每个试次独立延迟回报根本不存在gamma取0.9以上只会让更新信号更嘈杂。另一个常见原因是奖励矩阵的NoGo正确奖励给太高比如也设1导致模型频繁在“Go刺激下按NoGo”和“正确策略”之间摇摆——因为NoGo动作的平均回报被高估了。还有一种隐蔽原因是Q表初始值不是全0比如把NoGo状态的Go动作初始化成一个正数这会让模型一开始就偏向NoGo刺激也按Go破坏了学习过程。解决依次是gamma降到0.85到0.9NoGo正确奖励控制在0.5以下Q表务必全0初始化。这三步做完震荡通常会明显缓解。5.4 现象Double Q-learning版本效果反而变差改成Double Q-learning后正确率比标准Q-learning还低。原因在于GONOGO试次数少两张Q表各自访问次数减半估计方差反而增大。这不是算法本身的错是数据量撑不起两套统计量。解决试次数少于200时用标准Q-learning更稳多于300再上Double Q。另外注意Q1和Q2的更新要随机交替不要固定先Q1后Q2否则会引入周期性偏差。检查代码里是否用了rand 0.5而不是if mod(t, 2) 0这种固定交替。5.5 现象旧版本Matlab报错如果你用的是R2016b之前的版本movmean滑动平均不存在会直接报Undefined function。用AI编程工具生成的Matlab代码也经常默认新语法比如字符串用双引号、加号拼接字符串、missing占位符放在旧机器上就是一堆报错。我的习惯是代码里凡是R2016a之前引入的函数都写成循环或arrayfun兼容。检查方法很简单Matlab文档里每个函数页都标了“Introduced in R2016a”这样的版本信息批量核对一遍就好。注意跑模型前先确认Matlab版本再确认代码里没有用到比这个版本更新的语法。这条最基础但也最容易被忽略。6. 验证模型有没有学对行为曲线对齐、Q值热图与参数敏感性6.1 先看行为曲线对齐再看Q值热图训练结束后先做两件事把真实被试逐试次正确率的滑动平均和模型仿真用同一试次序列的正确率滑动平均画在一张图上。两条线形状一致说明模型捕捉到了学习动态。不一致时优先检查早期试次如果真实被试前20个试次正确率已经很高比如大于0.8说明模型的初始epsilon太高要调低如果真实被试后期仍有大量NoGo误按说明q2惩罚力度不够要调大NoGo误按的绝对值。第二步画Q值热图。2状态乘2动作的Q表就4个格子期望看到Q(Go刺激, Go动作)最大Q(NoGo刺激, NoGo动作)其次Q(NoGo刺激, Go动作)最负。如果热图显示Q(Go刺激, NoGo动作)比Q(Go刺激, Go动作)还高说明奖励函数里的NoGo正确奖励给太高或者gamma和alpha搭配让模型产生了幻觉。% 画Q值热图 imagesc(Q); colorbar; set(gca, XTick, 1:2, XTickLabel, {Go, NoGo}); set(gca, YTick, 1:2, YTickLabel, {Go刺激, NoGo刺激}); xlabel(动作); ylabel(状态); title(训练后Q值热图);对Double Q-learning记得画(Q1Q2)/2的平均热图而不是只画某一张。6.2 参数敏感性alpha、gamma和奖励系数怎么影响收敛速度参数敏感性分析是论文里必须有的部分。做法是每次只改一个参数固定其余跑多组随机种子看100试次后的末段正确率怎么变。alpha从0.05到0.5末段正确率通常呈倒U形gamma从0.5到0.99影响不大但要警惕0.99的震荡epsilon_min从0.01到0.2越小末段正确率越高但早期探索越少。把这些曲线用origin画成带置信区间的图就是发表级参数分析的底稿。真正让我学到东西的是跑敏感性分析时的发现alpha在0.2附近、gamma等于0.9、q2等于1.0的参数组合在几乎所有被试上都能复现出“Go错误率低于No-Go错误率”的经典行为模式而对称奖励版本无论怎么调参都做不到。这让我确认不对称奖励不是一个锦上添花的技巧而是GONOGO任务建模的必要条件。每次拿到新的行为数据我会先跑一遍标准Q-learning再看Q值热图最后做参数敏感性分析。这套流程帮我省掉了大量“调参调了一周发现是奖励矩阵写错”的血泪时间希望帮到你。本文还有配套的精品资源点击获取
返回列表