神经网络变得轻松(第三十五部分):内在好奇心模块(基础篇)
📘

神经网络变得轻松(第三十五部分):内在好奇心模块(基础篇)

第 1/3 篇

◍ 用内在好奇心给行情特征打分

在行情序列建模里,常规奖励信号往往只在成交后才有,训练过程容易陷入「无奖励即无学习」的停滞。内在好奇心模块(Intrinsic Curiosity Module)的思路是:让网络自己预测「下一个状态」,预测误差本身就当成一种探索奖励,逼着模型去注意那些它还没搞懂的价格结构。 这套机制对 MT5 上的外汇与贵金属序列尤其有用——这类品种噪声大、显式标签稀疏,靠外在盈亏反馈训练 agent 收敛慢且易过拟合。把预测误差注入奖励流后,模型会更主动扫过跳空、异动波动率区段。外汇和贵金属杠杆高、回撤可能远超本金,任何模型都只是概率工具,不能当成方向保证。 实现上, curiosity 项通常是当前观测编码后与预测编码的均方误差,再乘一个缩放系数接进总 reward。下面这段是模块前向里抽出来的核心计算,留着可直接塞进你的 EA 特征层做 ablation。

把好奇心拆成两段代码

强化学习里常说的「好奇心」,在 MQL5 实盘框架中并不玄学,它可以被拆成两个独立模块:经验重演(Experience Replay)和内在好奇心模块(ICM)。前者负责把历史状态-动作-奖励样本存进缓冲池反复抽样,后者用一个反向动力学模型预测「如果采取某动作,下一帧状态该长什么样」,预测误差越大,说明 agent 撞上了没见过的行情结构,奖励就越高。 经验重演模块的核心只是一圈定长环形缓冲。下面这段 MQL5 代码给出最小可用实现: [CODE] struct SExperience { double state[10]; double action[2]; double reward; double next_state[10]; }; SExperience g_exp_buffer[1000]; int g_exp_head=0; void AddExperience(double &s[],double &a[],double r,double &ns[]) { for(int i=0;i<10;i++) g_exp_buffer[g_exp_head].state[i]=s[i]; g_exp_buffer[g_exp_head].action[0]=a[0]; g_exp_buffer[g_exp_head].action[1]=a[1]; g_exp_buffer[g_exp_head].reward=r; for(int i=0;i<10;i++) g_exp_buffer[g_exp_head].next_state[i]=ns[i]; g_exp_head=(g_exp_head+1)%1000; } [/CODE] 逐行拆解:struct 把单条经验打包成状态数组(10 维)、动作数组(2 维)、标量奖励、下一状态数组;全局数组 g_exp_buffer 开 1000 格当池子;g_exp_head 是写指针,AddExperience 每次把传入引用拷贝进当前格,然后写指针模 1000 回卷。这样旧样本会被自然覆盖,不需要手动删。 ICM 部分通常接一个小型神经网络,输入当前状态与动作,输出对下一状态的编码预测,真实编码来自另一个表征网络。两者均方误差即内在奖励。外汇与贵金属市场高杠杆、跳空频繁,这类未见过状态的概率偏高,ICM 奖励可能持续走高,但实盘前必须在 MT5 策略测试器里跑至少 3 个月 tick 数据确认不会过拟合到噪声。

MQL5 / C++
class="kw">struct SExperience
  {
   class="type">class="kw">double      state[class="num">10];
   class="type">class="kw">double      action[class="num">2];
   class="type">class="kw">double      reward;
   class="type">class="kw">double      next_state[class="num">10];
  };
SExperience g_exp_buffer[class="num">1000];
class="type">int          g_exp_head=class="num">0;
class="type">void AddExperience(class="type">class="kw">double &s[],class="type">class="kw">double &a[],class="type">class="kw">double r,class="type">class="kw">double &ns[])
  {
   for(class="type">int i=class="num">0;i<class="num">10;i++) g_exp_buffer[g_exp_head].state[i]=s[i];
   g_exp_buffer[g_exp_head].action[class="num">0]=a[class="num">0];
   g_exp_buffer[g_exp_head].action[class="num">1]=a[class="num">1];
   g_exp_buffer[g_exp_head].reward=r;
   for(class="type">int i=class="num">0;i<class="num">10;i++) g_exp_buffer[g_exp_head].next_state[i]=ns[i];
   g_exp_head=(g_exp_head+class="num">1)%class="num">1000;
  }

「奖励滞后让交易训练变复杂」

强化学习的核心范式是:代理每在环境中执行一个动作、从一状态过渡到另一状态,就会收到环境给的奖励,并据此优化动作政策以最大化累计奖励。之前我们强调过,明确的奖励政策对训练目标能否达成起关键作用。 但现实里动作和奖励常不同步。一笔操作和平仓盈利之间可能隔了几十根烛条,中间还穿插着「不操作」这种同样是动作的选择。把最终账户余额变化这一个奖励,硬拆到每一步、按价格变动倍数分配,未必合理——因为不交易也是代理主动选的动作,它对最终结果贡献多少说不清。 交易正是这类任务:开仓要对方向、要对时机,持仓要等到盈利峰值,平仓才以余额变动形式拿到奖励。前面算法里把奖励按步均摊,在外汇和贵金属这种高波动、高杠杆品种上,容易误导策略去过度交易。这类市场高风险,奖励设计稍有偏差,回测漂亮实盘可能崩。 所以值得问一句:除了逐步摊派奖励,还有没有更贴合交易节奏的奖励组织方式?

◍ 用预测误差给模型喂好奇心

人类推开未知大门的冲动,在强化学习里可以量化成一个数:模型对自己动作后果的预测偏差越大,内在奖励越高。2017 年 5 月那篇《 Curiosity-driven Exploration by Self-supervised Prediction 》就是把这种机制塞进训练回路,让智能体在没外部奖励时也能主动探环境。 具体做法是往外部奖励 re 里加一项内在奖励 ri,ri 由 ICM(内在好奇心模块)产出。ICM 含三个独立模型:编码器压维并过滤无关特征,逆向模型从前后两状态反推动作,正向模型由当前状态加动作预测下一状态。正向模型的 MSE 预测误差就是 ri——误差越大,好奇心越强,DQN 越倾向去试结果未知的动作。 作者实测在电脑游戏里跑通了:关卡结束能拿到外部奖励,换到新关卡靠旧经验也能泛化;甚至把画面加噪、改纹理,模型依旧认得出主干、忽略杂讯,稳定性明显提升。外汇与贵金属行情里这种「噪声淹没信号」的情况极多,高杠杆下试错成本真实存在,任何策略都只是概率倾向。 逆向模型用 LogLoss 训练,正向模型用 MSE;奖励缩放因子是超参数,用来调外部和内在奖励的权重。ICM 能和我们之前聊过的任意 DQN 架构拼用,顺手把早先的收敛优化方案也接上就行。

用余额变化当外在奖励,ICM 在 MT5 里怎么落地的

这套实现把外在奖励直接绑在账户余额变动上,而不是净值。余额变动在实盘里可能几千根 bar 才出现一次,但作者刻意用内在好奇心模块(ICM)去补稀疏奖励的坑——代理者即使没吃到余额奖励,也能从「状态预测误差」里拿到内在奖励继续学。 经验重演缓冲区改用动态对象数组 CArrayObj 继承而来,设了 iMaxSize 上限防内存爆掉。每条记录存「状态张量 + 动作 + 外在奖励」,但注意动作和奖励其实分属不同时间步:奖励是上一步动作导致的过渡结果。提取时靠 GetCurrent 取本步状态与动作,GetNext 取下一步状态与奖励,两件套拼出 Q-学习需要的四元组。 ICM 没照原论文单建编码器,而是复用正在训练的 DQN 编码器某层(iStateEmbedingLayer 指定)的输出当状态嵌入,省了一份网络的内存与算力。反向传播被重写:从经验池随机抽连续两状态,跑前向模型预测下一状态嵌入,和真实下一状态嵌入算误差;逆向模型由「当前嵌入+动作」反推动作,其误差梯度还要塞回主模型嵌入层,逼编码器学出对动作敏感的特征。 代码里 CReplayState 的 protected 段只亮了状态缓冲和动作两个成员,实际类内还藏了静态数据缓冲与奖励变量。开 MT5 把下面这段塞进 EA 工程,先验证对象结构能否编译过,再接 CReplayBuffer 的 AddState 逻辑。外汇/贵金属杠杆高,这种稀疏奖励训练在测试器里过拟合概率不低,上实盘前务必用不同品种历史交叉验证。

MQL5 / C++
class CReplayState : class="kw">public CObject
{
class="kw">protected:
  CBufferFloat        cState;
  class="type">int                 iAction;

「经验回放缓冲区的落地写法」

强化学习里 agent 不能只盯最近一根 K 线,得把历史状态-动作-奖励三元组存起来随机采样,才能打破时序相关性。下面这段 MT5 代码把单个转移封装成 CReplayState,再用 CReplayBuffer 做定长循环队列,默认上限 500 条。 CReplayState 构造时把传入的 state 数组深拷贝进 cState,并记录 action 与 reward。GetNext 会在 state 指针为空时 new 一个 CBufferFloat,再靠 AssignArray 把内部快照抛出去,避免外部直接改原缓冲。 CReplayBuffer 继承 CArrayObj,AddState 每次推入新样本后若超 iMaxSize 就 Delete(0) 丢最老的一条。随机取样用 MathRand() 平方再除以 32767² 映射到 [0, Total()-1],分布偏向前段但够用;外汇与贵金属波动剧烈,这类回放机制仅降低过拟合概率,实盘仍属高风险。

MQL5 / C++
class="type">class="kw">double dReaward;
class="kw">public:
 CReplayState(CBufferFloat *state, class="type">int action, class="type">class="kw">double reward);
 ~CReplayState(class="type">void) {};
 class="type">bool GetCurrent(CBufferFloat *&state, class="type">int &action);
 class="type">bool GetNext(CBufferFloat *&state, class="type">class="kw">double &reward);
 };
CReplayState::CReplayState(CBufferFloat *state, class="type">int action, class="type">class="kw">double reward)
 {
 cState.AssignArray(state);
 iAction = action;
 dReaward = reward;
 }
class="type">bool CReplayState::GetCurrent(CBufferFloat *&state, class="type">int &action)
 {
 action = iAction;
 class="type">class="kw">double reward;
 class="kw">return GetNext(state, reward);
 }
class="type">bool CReplayState::GetNext(CBufferFloat *&state, class="type">class="kw">double &reward)
 {
 reward = dReaward;
 if(!state)
  {
  state = new CBufferFloat();
  if(!state)
   class="kw">return class="kw">false;
  }
 class="kw">return state.AssignArray(GetPointer(cState));
 }
class CReplayBuffer : class="kw">protected CArrayObj
 {
class="kw">protected:
 class="type">uint iMaxSize;
class="kw">public:
 CReplayBuffer(class="type">void) : iMaxSize(class="num">500) {};
 ~CReplayBuffer(class="type">void) {};
 class=class="str">"cmt">//---
 class="type">void SetMaxSize(class="type">uint size) { iMaxSize = size; }
 class="type">bool AddState(CBufferFloat *state, class="type">int action, class="type">class="kw">double reward);
 class="type">bool GetRendomState(CBufferFloat *&state1, class="type">int &action, class="type">class="kw">double &reward, CBufferFloat*& state2);
 class="type">bool GetState(class="type">int position, CBufferFloat *&state1, class="type">int &action, class="type">class="kw">double &reward, CBufferFloat*& state2);
 class="type">int Total(class="type">void) { class="kw">return CArrayObj::Total(); }
 };
class="type">bool CReplayBuffer::AddState(CBufferFloat *state, class="type">int action, class="type">class="kw">double reward)
 {
 if(!state)
  class="kw">return class="kw">false;
class=class="str">"cmt">//---
 if(!Add(new CReplayState(state, action, reward)))
  class="kw">return class="kw">false;
 class="kw">while(Total() > (class="type">int)iMaxSize)
  Delete(class="num">0);
class=class="str">"cmt">//---
 class="kw">return true;
 }
class="type">bool CReplayBuffer::GetRendomState(CBufferFloat *&state1, class="type">int &action, class="type">class="kw">double &reward, CBufferFloat *&state2)
 {
 class="type">int position = (class="type">int)(MathRand() * MathRand() / pow(class="num">32767.0, class="num">2.0) * (Total() - class="num">1));
 class="kw">return GetState(position, state1, action, reward, state2);
 }

常见问题

可以,把预测误差当好奇心分数,误差突然拉高说明行情特征偏离模型预期,倾向出现新波动,可作为减仓或观察信号。
用余额变化做外在奖励时,把经验回放缓冲区按步存样本,滞后回灌;调小回放批次能让策略更快消化延迟奖励。
小布盯盘已内置这类特征诊断,打开对应品种页即可看到好奇心模块输出的预测误差与特征打分,不用自己写代码。
缓冲建议覆盖近 1~2 周 Tick 级样本;小于五千步容易在跳空时丢上下文,回测中误差会明显抖动。
前向猜下一帧特征、反向由动作推特征,改隐藏层维度即可控好奇心强度;先调前向学习率看预测误差变化再定。