神经网络变得轻松(第三十五部分):内在好奇心模块·进阶篇
(2/3)· 当平仓才给奖励太滞后,如何让智能体在每一步都主动探索行情
「从经验池里抽四元组交给网络」
强化学习里智能体不是逐根 K 线硬记,而是把(state1, action, reward, state2)四元组塞进回放缓冲,再随机抽批次去训网络。下面这段取数函数就是干这个脏活:给定 position,把前后两个状态指针和中间动作、奖励一并吐出来。 边界检查先卡死:position 小于 0 或大于等于 Total()-1 直接返回 false,因为拿 state2 必须占用下一格,最后一格没后继状态可用。 内部用 m_data[position] 取当前帧,element.GetCurrent 填 state1 和 action;再取 position+1 的下一帧,GetNext 填 state2 和 reward。任何一步空指针或解析失败都短路返回 false,全过才返回 true。 CICM 类把这套逻辑包成了内在好奇心模块:它同时挂了 cTargetNet、cInverseNet、cForwardNet 三张网,iStateEmbedingLayer 控制状态嵌入层位置,dPrevBalance 记上一帧权益用来算 reward。backProp 的默认 discount=0.9f 是折扣因子,调小会更看重眼前盈亏。 开 MT5 把这段 GetState 抄进你的 EA,在 OnTick 里打印 position=0 和 position=Total()-2 的返回值,能立刻看出缓冲有没有被正确填帧。外汇和贵金属杠杆高,训出来的策略也可能在实盘失效,先用历史数据验证逻辑。
class="type">bool CReplayBuffer::GetState(class="type">int position, CBufferFloat *&state1, class="type">int &action, class="type">class="kw">double &reward, CBufferFloat *&state2) { if(position < class="num">0 || position >= (Total() - class="num">1)) class="kw">return class="kw">false; CReplayState* element = m_data[position]; if(!element || !element.GetCurrent(state1, action)) class="kw">return class="kw">false; element = m_data[position + class="num">1]; if(!element.GetNext(state2, reward)) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return true; } class CICM : class="kw">protected CNet { class="kw">protected: class="type">uint iMinBufferSize; class="type">uint iStateEmbedingLayer; class="type">class="kw">double dPrevBalance; class=class="str">"cmt">//--- CReplayBuffer cReplay; CNet cTargetNet; CNet cInverseNet; CNet cForwardNet; class="kw">virtual class="type">bool AddInputData(CArrayFloat *inputVals); class="kw">public: CICM(class="type">void); CICM(CArrayObj *Description, CArrayObj *Forward, CArrayObj *Inverse); class="type">bool Create(CArrayObj *Description, CArrayObj *Forward, CArrayObj *Inverse); class="type">int feedForward(CArrayFloat *inputVals, class="type">int window = class="num">1, class="type">bool tem = true, class="type">bool sample = true); class="type">bool backProp(class="type">int batch, class="type">class="kw">float discount = class="num">0.9f); class="type">int getAction(class="type">void); class="type">int getSample(class="type">void); class="type">class="kw">float getRecentAverageError() { class="kw">return recentAverageError; } class="type">bool Save(class="type">class="kw">string file_name, class="type">bool common = true); class="type">bool Save(class="type">class="kw">string dqn, class="type">class="kw">string forward, class="type">class="kw">string invers, class="type">bool common = true); class="kw">virtual class="type">bool Load(class="type">class="kw">string file_name, class="type">bool common = true); class="type">bool Load(class="type">class="kw">string dqn, class="type">class="kw">string forward, class="type">class="kw">string invers, class="type">uint state_layer, class="type">bool common = true); class=class="str">"cmt">//--- class="kw">virtual class="type">int Type(class="type">void) class="kw">const { class="kw">return defICML; } class="kw">virtual class="type">bool TrainMode(class="type">bool flag)
回放池没攒够就别急着反向传播
CICM 的 backProp 方法里有个硬门槛:只有当 cReplay 里存的状态数大于等于 iMinBufferSize 时,才会真正进训练循环,否则直接返回 true 跳过本次更新。这个设计避免了用极少样本去扰动网络权重,在 MT5 里若把 iMinBufferSize 设得太小,模型可能过早过拟合近端噪声。 折扣因子在代码里写死为 0.9f(discount = 0.900000f),意味着未来奖励按每步 10% 衰减计入 Q 值目标。你可以直接改这个浮点参数观察回测中持仓偏好的变化,外汇与贵金属杠杆高,这类强化学习策略实盘前务必用历史Tick充分验证,亏损概率不低。 训练循环每次从 cReplay.GetRendomState 抽随机状态三元组(state1, action, reward, state2),先对 state1 做 CNet::feedForward 拿当前 Q,再取嵌入层输出。下面这段是 backProp 入口与门槛判断的原样代码,逐行看逻辑比读文档快: bool CICM::backProp(int batch, float discount = 0.900000f) { //--- if(cReplay.Total() < (int)iMinBufferSize) return true; if(!UpdateTarget(TargetNetFile)) return false;
class="type">bool CICM::backProp(class="type">int batch, class="type">class="kw">float discount = class="num">0.900000f) { class=class="str">"cmt">//--- if(cReplay.Total() < (class="type">int)iMinBufferSize) class="kw">return true; if(!UpdateTarget(TargetNetFile)) class="kw">return class="kw">false;
◍ 双网络结构下的动作向量拼接与反向传播
这段逻辑跑在强化学习智能体的训练循环里:先把选中的动作做成 one-hot 向量,再和当前状态向量拼起来送进前向网络。代码里 actions 用 Zeros 初始化后把 action 下标置 1,AssignArray 拼接到 state1 末尾,任何一步失败直接 return false,保证张量维度不对时及早退出。 前向网络 cForwardNet 用 feedForward(targetVals,1,false) 做下一状态预测,目标网络 cTargetNet 则对 state2 做前向得到嵌入,再取 iStateEmbedingLayer 的输出回灌。注意这里 state1.AddArray(state2) 把两个嵌入直接concat后丢给逆网络,逆网络负责从拼接状态反推执行过的动作,feedForward 和 backProp 都带 false 标志说明不更新目标网权重。 奖励项用 MathPow(st2-st1,2).Sum() 累加预测状态差的平方,再按 reward + discount * targetVals.Maximum() 写回 target 数组。discount 是折扣因子,若设 0.9,远期奖励会按每层 10% 衰减,调这个参数能明显改变策略偏好。 主网络的反向传播按层倒序计算:先取最后一层神经元算输出梯度,recentAverageError 用平滑因子 recentAverageSmoothingFactor 做滑动平均,fmin 防止初期步数少时分母过大。遍历 layerNum 从 total-2 到 0 隐藏层梯度,这段没贴完,但已经能看出训练时误差是 LOSS_MSE 驱动。外汇与贵金属市场高风险,这类模型回测盈利不代表实盘概率占优,开 MT5 把 discount 和 smoothing factor 改成自己的值跑一遍才知深浅。
class="kw">return class="kw">false; class=class="str">"cmt">//--- prepare a one-hot action vector and concatenate with the current state vector getResults(target); actions = vector<class="type">class="kw">float>::Zeros(target.Size()); actions[action] = class="num">1; if(!targetVals.AssignArray(actions) || !targetVals.AddArray(state1)) class="kw">return class="kw">false; class=class="str">"cmt">//--- forward net feed forward pass - next state prediction if(!cForwardNet.feedForward(targetVals, class="num">1, class="kw">false)) class="kw">return class="kw">false; class=class="str">"cmt">//--- feed forward if(!cTargetNet.feedForward(state2, class="num">1, class="kw">false)) class="kw">return class="kw">false; class=class="str">"cmt">//--- unload the state embedding and concatenate with the "current" state embedding if(!cTargetNet.GetLayerOutput(iStateEmbedingLayer, state2)) class="kw">return class="kw">false; class=class="str">"cmt">//--- inverse net feed forward - defining the performed action. if(!state1.AddArray(state2) || !cInverseNet.feedForward(state1, class="num">1, class="kw">false)) class="kw">return class="kw">false; class=class="str">"cmt">//--- inverse net backpropagation if(!targetVals.AssignArray(actions) || !cInverseNet.backProp(targetVals)) class="kw">return class="kw">false; class=class="str">"cmt">//--- forward net backpropagation if(!cForwardNet.backProp(state2)) class="kw">return class="kw">false; class=class="str">"cmt">//--- reward adjustment cForwardNet.getResults(st1); state2.GetData(st2); reward += (MathPow(st2 - st1, class="num">2)).Sum(); cTargetNet.getResults(targetVals); target[action] = (class="type">class="kw">float)(reward + discount * targetVals.Maximum()); if(!targetVals.AssignArray(target)) class="kw">return class="kw">false; class=class="str">"cmt">//--- backpropagation pass of the model being trained { getResults(result); class="type">class="kw">float error = result.Loss(target, LOSS_MSE); class=class="str">"cmt">//--- currentLayer = layers.At(layers.Total() - class="num">1); if(CheckPointer(currentLayer) == POINTER_INVALID) class="kw">return class="kw">false; neuron = currentLayer.At(class="num">0); if(!neuron.calcOutputGradients(targetVals, error)) class="kw">return class="kw">false; class=class="str">"cmt">//--- backPropCount++; recentAverageError += (error - recentAverageError) / fmin(recentAverageSmoothingFactor, (class="type">class="kw">float)backPropCount); class=class="str">"cmt">//--- Calc Hidden Gradients class="type">int total = layers.Total(); for(class="type">int layerNum = total - class="num">2; layerNum >= class="num">0; layerNum--) {
「反向传播里梯度与权重更新的内核调用」
这段逻辑跑在神经网络训练的反向阶段:先逐层倒推隐藏层梯度,遇到指定的嵌入层时,把正向网络神经元梯度与逆网络对应梯度在 OpenCL 端做矩阵相加,再统一更新输入权重。 nextLayer = currentLayer; 先把当前层缓存成下一层引用,currentLayer 再指向 layers.At(layerNum) 取本层,neuron 取该层第 0 个神经元。若 neuron.calcHiddenGradients(nextLayer.At(0)) 返回 false 直接退出,说明梯度计算失败。 当 layerNum == iStateEmbedingLayer 时,从 cInverseNet.layers.At(0) 取出逆网络首层神经元 inv,用 neuron.Neurons() 作为 global_work_size[0](即一维并行粒度 = 本层神经元数)。随后把 neuron 梯度缓冲、inv 梯度缓冲和输出缓冲都绑到 def_k_MatrixSum 内核,乘子设 1,执行后若失败则 printf 错误码并返回 false。 梯度算完才进权重更新:prevLayer 初始为最后一层,从后往前遍历,currentLayer 取 prevLayer、prevLayer 再前移一层,neuron.UpdateInputWeights(prevLayer.At(0)) 完成一次输入权重修正,任意一步失败即终止。最后一层循环把各层 currentLayer.At(0) 取出做收尾,训练步的反向闭环到此为止。 在 MT5 里跑这套,重点看 global_work_size[0] 是否等于神经元数——若自定义层结构后梯度维度对不上,OpenCL 执行会静默报错,建议加 printf 把 Neurons() 打出来核对。外汇与贵金属模型训练涉及高杠杆风险,回测收益不代表实盘概率。
nextLayer = currentLayer; currentLayer = layers.At(layerNum); neuron = currentLayer.At(class="num">0); if(!neuron.calcHiddenGradients(nextLayer.At(class="num">0))) class="kw">return class="kw">false; if(layerNum == iStateEmbedingLayer) { CLayer* temp = cInverseNet.layers.At(class="num">0); CNeuronBaseOCL* inv = temp.At(class="num">0); class="type">uint global_work_offset[class="num">1] = {class="num">0}; class="type">uint global_work_size[class="num">1]; global_work_size[class="num">0] = neuron.Neurons(); opencl.SetArgumentBuffer(def_k_MatrixSum, def_k_sum_matrix1, neuron.getGradientIndex()); opencl.SetArgumentBuffer(def_k_MatrixSum, def_k_sum_matrix2, inv.getGradientIndex()); opencl.SetArgumentBuffer(def_k_MatrixSum, def_k_sum_matrix_out, neuron.getGradientIndex()); opencl.SetArgument(def_k_MatrixSum, def_k_sum_dimension, class="num">1); opencl.SetArgument(def_k_MatrixSum, def_k_sum_multiplyer, class="num">1); if(!opencl.Execute(def_k_MatrixSum, class="num">1, global_work_offset, global_work_size)) { printf("Error of execution kernel MatrixSum: %d", GetLastError()); class="kw">return class="kw">false; } } class=class="str">"cmt">//--- Calc Hidden Gradients class="type">int total = layers.Total(); for(class="type">int layerNum = total - class="num">2; layerNum >= class="num">0; layerNum--) { class=class="str">"cmt">//--- prevLayer = layers.At(total - class="num">1); for(class="type">int layerNum = total - class="num">1; layerNum > class="num">0; layerNum--) { currentLayer = prevLayer; prevLayer = layers.At(layerNum - class="num">1); neuron = currentLayer.At(class="num">0); if(!neuron.UpdateInputWeights(prevLayer.At(class="num">0))) class="kw">return class="kw">false; } class=class="str">"cmt">//--- for(class="type">int layerNum = class="num">0; layerNum < total; layerNum++) { currentLayer = layers.At(layerNum); CNeuronBaseOCL *temp = currentLayer.At(class="num">0);
训练态与梯度缓冲的退出判定
这段片段处在某个网络前向/反向流程的收口位置,重点在于对单层状态做提前跳出与资源释放。若当前临时层未处于 TrainMode(),直接 continue 跳过后续计算,避免在推理或挂起态下误跑权重更新。 当处理到倒数第一层(layerNum+1 == total)且梯度缓冲未成功 BufferRead() 时,函数直接 return false,说明梯度管道没就绪就不能继续反向传播,否则会得到脏梯度。 循环结束后依次 delete 掉 state1、state2、targetVals 三个指针,防止 MT5 策略测试器里反复调用时堆内存累积;最后 return true 表示整段训练态组装无误。 在 MT5 里把这段嵌进你自己的 CLayer 容器跑一遍,若日志里频繁卡在 return false,优先查最后一层 getGradient().BufferRead() 的缓冲区初始化时机,而不是怀疑学习率。
if(!temp.TrainMode()) class="kw">continue; if((layerNum + class="num">1) == total && !temp.getGradient().BufferRead()) class="kw">return class="kw">false; class="kw">break; } } } class="kw">delete state1; class="kw">delete state2; class="kw">delete targetVals; class=class="str">"cmt">//--- class="kw">return true; }
◍ 在策略测试器里跑通内在好奇心模型
把前面封装好的 Q-学习类真正喂进行情,靠的是 ICM-learning.mq5 这个 EA。它不在初始化时启动训练,而是把整套学习流程搬到了 OnTick,只在每根新烛条开盘时触发一次,避免重复计算。 EA 外部参数和指标(RSI、CCI、ATR、MACD)沿用了之前设置,描述市场状态的特征向量维度仍是 12。训练用的历史窗口由 HistoryBars 控制,每次新柱触发后先 CopyRates 载入对应长度,再逐根拼装收盘价偏移、高低偏移、成交量、时间结构和四大指标值。 feedForward 返回 0/1/2 三种动作分别对应买、卖和第三个方向,第四个“观望”动作不落地成交易,所以代码里只处理三种。模型反向传播放在方法末尾,而训练好的权重不实时写盘,统一挪到逆初始化(OnDeinit)和优化每步验算后保存。 模型文件必须丢进终端公共目录 Terminal\Common\Files,因为每个测试代理跑在独立沙箱,只能靠公共文件夹交换数据。优化时务必只开单内核,并行线程会互相清掉对方代理的数据,多代理机制直接失效。 实测用 EURUSD H1、默认指标参数,训练期压缩到 10 个月。第一次优化验算净值接近 0,第二次开始转正,整段跑了 330 笔交易,盈利操作占比超 98%。外汇与贵金属系高杠杆品种,这类回测结果仅代表特定样本内的概率倾向,实盘可能显著偏离。
class="type">void OnTick() { if(!IsNewBar()) class="kw">return; class="type">int bars = CopyRates(Symb.Name(), TimeFrame, iTime(Symb.Name(), TimeFrame, class="num">1), HistoryBars, Rates); if(!ArraySetAsSeries(Rates, true)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); class="kw">return; } class=class="str">"cmt">//--- RSI.Refresh(); CCI.Refresh(); ATR.Refresh(); MACD.Refresh(); State1.Clear(); for(class="type">int b = class="num">0; b < (class="type">int)HistoryBars; b++) { class="type">class="kw">float open = (class="type">class="kw">float)Rates[b].open; TimeToStruct(Rates[b].time, sTime); class="type">class="kw">float rsi = (class="type">class="kw">float)RSI.Main(b); class="type">class="kw">float cci = (class="type">class="kw">float)CCI.Main(b); class="type">class="kw">float atr = (class="type">class="kw">float)ATR.Main(b); class="type">class="kw">float macd = (class="type">class="kw">float)MACD.Main(b); class="type">class="kw">float sign = (class="type">class="kw">float)MACD.Signal(b); if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE) class="kw">continue; class=class="str">"cmt">//--- if(!State1.Add((class="type">class="kw">float)Rates[b].close - open) || !State1.Add((class="type">class="kw">float)Rates[b].high - open) || !State1.Add((class="type">class="kw">float)Rates[b].low - open) || !State1.Add((class="type">class="kw">float)Rates[b].tick_volume / class="num">1000.0f) || !State1.Add(sTime.hour) || !State1.Add(sTime.day_of_week) || !State1.Add(sTime.mon) || !State1.Add(rsi) || !State1.Add(cci) || !State1.Add(atr) || !State1.Add(macd) || !State1.Add(sign)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); class="kw">break; } } class="kw">switch(StudyNet.feedForward(GetPointer(State1), class="num">12, true, true)) { case class="num">0: Trade.Buy(Symb.LotsMin(), Symb.Name()); class="kw">break; case class="num">1: Trade.Sell(Symb.LotsMin(), Symb.Name()); class="kw">break; case class="num">2: