神经网络变得轻松(第二十八部分):政策梯度算法·进阶篇
📘

神经网络变得轻松(第二十八部分):政策梯度算法·进阶篇

第 2/3 篇

用 OpenCL 把 SoftMax 塞进神经网络层

SoftMax 没走常规激活函数的老路,而是单独建了个 CNeuronSoftMaxOCL 类挂在 CNeuronBaseOCL 下面。它不另开缓冲区,连构造析构都留空,只重写 feedForward 和 calcOutputGradients——因为换了损失函数,梯度回传必须自己算。 前馈放到 OpenCL 内核 SoftMax_FeedForward 里跑。难点在指数求和:每个线程各算一部分再合并。局部内存数组大小得在内核创建时定死,这直接限制了能参与求和的线程数。求和用两段循环,第一段各线程按步长扫全向量存进局部数组,barrier 同步;第二段不断对半加,直到索引 0 拿到总和。 反向传播分两个内核。SoftMax_HiddenGradient 里每个输入元素要从所有输出元素收误差份额,用私密变量存中间值提速;SoftMax_OutputGradient 配 LogLoss,对数导数就是 1 除以参数,两行写完。 [CODE] class CNeuronSoftMaxOCL : public CNeuronBaseOCL { protected: virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override { return true; } public: CNeuronSoftMaxOCL(void) {}; ~CNeuronSoftMaxOCL(void) {}; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL); virtual bool calcOutputGradients(CArrayFloat *Target, float error) override; //--- [/CODE] 代码逐行拆解: 第1行:类声明,派生自神经元基类。 第4行:feedForward 重写,前馈验算在此实现。 第5行:updateInputWeights 直接返回 true,SoftMax 层不更新前层权重。 第7-8行:构造与析构均为空。 第9行:calcInputGradients 声明,用于隐藏层梯度。 第10行:calcOutputGradients 重写,接目标向量与误差算输出梯度。 主程序里补上内核常量、声明和调用方法,算法照搬以往类似结构。到这层,SoftMax 已能接进 MT5 神经网络,下一步是写 EA 把政策梯度模型跑起来训练。

MQL5 / C++
class CNeuronSoftMaxOCL      :  class="kw">public CNeuronBaseOCL
  {
class="kw">protected:
   class="kw">virtual class="type">bool      feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override;
   class="kw">virtual class="type">bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override { class="kw">return true; }
class="kw">public:
                    CNeuronSoftMaxOCL(class="type">void) {};
                   ~CNeuronSoftMaxOCL(class="type">void) {};
  class="kw">virtual class="type">bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL);  
  class="kw">virtual class="type">bool      calcOutputGradients(CArrayFloat *Target, class="type">float error) class="kw">override;
  class=class="str">"cmt">//---

「SoftMax 在 OpenCL 里的并行前向实现」

把 SoftMax 丢到 GPU 上跑,核心思路是用 local memory 做分段指数求和再归约。上面这段 OpenCL 内核里,每个 workgroup 最多吃 256 个局部线程(ls 被 min 到 256),先按 ls 步长把 inputs 的 exp 值累加到 temp[256] 局部数组,避免反复读全局显存。 归约部分用 do-while 把 count 折半:temp[l] 不断加上偏移 count 位置的值,barrier(CLK_LOCAL_MEM_FENCE) 保证同步,最终 temp[0] 拿到全段指数和 sum。若 sum 非零,再走一遍把每个 exp(inputs[shift]) 除以 sum+1e-37f 写回 outputs,1e-37f 就是防除零的极小值。 反向传播拆成两个内核。SoftMax_HiddenGradient 里对每个输出 i 跑全 outputs_total 的循环,套公式 outputs[j]*output_gr[j]*((i==j?1:0)-output),这是标准 SoftMax 雅可比乘梯度;SoftMax_OutputGradient 入口只取到 targets 参数,原文在此截断,但接口已经表明它负责把目标值与输出差直接算输出层梯度。 在 MT5 里验证时,把 total 设成你特征向量长度(比如 64 或 128),本地工作组大小别超 256,否则 temp 数组越界。外汇与贵金属模型用这套做多分类概率输出时,记得 GPU 浮点误差可能让 sum 偏小,概率分布会略有偏移,属高风险实验环境。

MQL5 / C++
class="kw">virtual class="type">int Type(class="type">void) class="kw">override const { class="kw">return defNeuronSoftMaxOCL; }
};
__kernel class="type">void SoftMax_FeedForward(__global class="type">float *inputs,
                                 __global class="type">float *outputs,
                                 const class="type">ulong total)
  {
   class="type">uint i = (class="type">uint)get_global_id(class="num">0);
   class="type">uint l = (class="type">uint)get_local_id(class="num">0);
   class="type">uint ls = min((class="type">uint)get_local_size(class="num">0), (class="type">uint)class="num">256);
class=class="str">"cmt">//---
   __local class="type">float temp[class="num">256];
   class="type">uint count = class="num">0;
   if(l < class="num">256)
     do
       {
        class="type">uint shift = count * ls + l;
        temp[l] = (count > class="num">0 ? temp[l] : class="num">0) + (count * ls + l < total ? exp(inputs[shift]) : class="num">0);
        count++;
       }
     while((count * ls + l) < total);
   barrier(CLK_LOCAL_MEM_FENCE);
   count = ls;
   do
     {
      count = (count + class="num">1) / class="num">2;
      if(l < class="num">256)
        temp[l] += (l < count && (l + count) < total ? temp[l + count] : class="num">0);
      barrier(CLK_LOCAL_MEM_FENCE);
     }
   while(count > class="num">1);
   class="type">float sum = temp[class="num">0];
   if(sum != class="num">0)
     {
      count = class="num">0;
      while((count * ls + l) < total)
        {
         class="type">uint shift = count * ls + l;
         outputs[shift] = exp(inputs[shift]) / (sum + 1e-37f);
         count++;
        }
     }
  }
__kernel class="type">void SoftMax_HiddenGradient(__global class="type">float* outputs,
                                     __global class="type">float* output_gr,
                                     __global class="type">float* input_gr)
  {
   class="type">size_t i = get_global_id(class="num">0);
   class="type">size_t outputs_total = get_global_size(class="num">0);
   class="type">float output = outputs[i];
   class="type">float result = class="num">0;
   for(class="type">int j = class="num">0; j < outputs_total; j++)
     result += outputs[j] * output_gr[j] * ((class="type">float)(i == j ? class="num">1 : class="num">0) - output);
   input_gr[i] = result;
  }
__kernel class="type">void SoftMax_OutputGradient(__global class="type">float* outputs,
                                     __global class="type">float* targets,

◍ softmax 梯度与强化训练骨架

这段 OpenCL 内核把 softmax 输出层的梯度直接写成 -targets[i] / (outputs[i] + 1e-37f),加 1e-37f 是为防除零而非数值平滑,MT5 里 float 极小值就在这个量级。 宏定义把 SoftMax 的前向、隐层梯度、输出层梯度分别编为 36/37/38 号内核,输入槽位用 0/1/2 固定映射;如果你改网络结构,先核对这些 def_k 偏移,否则 StudyNet 跑出来是全零梯度。 训练侧用 SesionSize = 24*22 = 528 根向量装动作与奖励,DiscountFactor = 0.999 控制远期奖励衰减;Train() 里先按 StudyPeriod 回拨年份取历史,bars 不足时直接 ExpertRemove(),实盘前要把 HistoryBars 和 SesionSize 的倍数关系算清,外汇与贵金属杠杆高,回测过拟合会放大实盘风险。 RSI/CCI/ATR/MACD 四个指标缓冲区按 bars resize,再 Refresh;total 被砍掉 HistoryBars + 2*SesionSize 根 K 线,意味着开头段永远不参与迭代。

MQL5 / C++
  __global class="type">float* output_gr)
  {
  class="type">size_t i = get_global_id(class="num">0);
  output_gr[i] = -targets[i] / (outputs[i] + 1e-37f);
  }
class="macro">#define def_k_SoftMax_FeedForward       class="num">36
class="macro">#define def_k_softmaxff_inputs          class="num">0
class="macro">#define def_k_softmaxff_outputs         class="num">1
class="macro">#define def_k_softmaxff_total           class="num">2
class=class="str">"cmt">//---
class="macro">#define def_k_SoftMax_HiddenGradient    class="num">37
class="macro">#define def_k_softmaxhg_outputs         class="num">0
class="macro">#define def_k_softmaxhg_output_gr       class="num">1
class="macro">#define def_k_softmaxhg_input_gr        class="num">2
class=class="str">"cmt">//---
class="macro">#define def_k_SoftMax_OutputGradient    class="num">38
class="macro">#define def_k_softmaxog_outputs         class="num">0
class="macro">#define def_k_softmaxog_targets         class="num">1
class="macro">#define def_k_softmaxog_output_gr       class="num">2
CNet                    StudyNet;
CArrayObj               States;
vectorf                 vActions;
vectorf                 vRewards;
input class="type">int               SesionSize =  class="num">24 * class="num">22;
input class="type">int               Iterations = class="num">1000;
input class="type">class="kw">double            DiscountFactor =  class="num">0.999;
  if(!vActions.Resize(SesionSize) ||
     !vRewards.Resize(SesionSize))
     class="kw">return INIT_FAILED;
class="type">void Train(class="type">void)
  {
class=class="str">"cmt">//---
  class="type">MqlDateTime start_time;
  TimeCurrent(start_time);
  start_time.year -= StudyPeriod;
  if(start_time.year <= class="num">0)
     start_time.year = class="num">1900;
  class="type">class="kw">datetime st_time = StructToTime(start_time);
  class="type">int bars = CopyRates(Symb.Name(), TimeFrame, st_time, TimeCurrent(), Rates);
  if(!RSI.BufferResize(bars) || !CCI.BufferResize(bars) || !ATR.BufferResize(bars) || !MACD.BufferResize(bars))
   {
     ExpertRemove();
     class="kw">return;
   }
  if(!ArraySetAsSeries(Rates, true))
   {
     ExpertRemove();
     class="kw">return;
   }
class=class="str">"cmt">//---
  RSI.Refresh();
  CCI.Refresh();
  ATR.Refresh();
  MACD.Refresh();
class=class="str">"cmt">//---
  class="type">int total = bars - (class="type">int)(HistoryBars + class="num">2 * SesionSize);
  CBufferFloat* State;
  for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped()); iter ++)
    {
      class="type">int error_code;

用正态抖动挑样本窗口喂给神经网络

这段逻辑干的事是先算一个随机偏移 shift:以均值0、标准差1的正态分布取绝对值,截断到[0,1]再乘 total 加 SesionSize,等于在历史区间里随机定位一段会话窗口。外汇与贵金属市场高杠杆、滑点无常,这种随机采样只能增加样本覆盖,不保证任何方向胜率。 随后清空 States 容器,按 SesionSize 批次循环,每批取 shift-batch 作为起点 i,新建 CBufferFloat 存单条状态;若内存分配失败直接 ExpertRemove 退出。每根样本会回看 HistoryBars 根 K 线,把 close/open、high/open、low/open 的差值,tick_volume/1000,以及小时、星期、月份和 RSI、CCI、ATR、MACD、Signal 共 12 个特征塞进缓冲。 任一指标读到 EMPTY_VALUE 就跳过该 bar;若最终 State 总数不足 HistoryBars*12,说明特征残缺,continue 弃用。最后调 StudyNet.feedForward 前向推理,失败同样移除 EA。你在 MT5 里把 HistoryBars 从 30 调到 60,会明显看到缓冲长度阈值从 360 变成 720,采样耗时可能翻倍。

MQL5 / C++
class="type">int shift = (class="type">int)(fmin(fabs(Math::MathRandomNormal(class="num">0,class="num">1,error_code)),class="num">1) * (total) + SesionSize);
States.Clear();
for(class="type">int batch = class="num">0; batch < SesionSize; batch++)
  {
   class="type">int i = shift - batch;
   State = new CBufferFloat();
   if(!State)
     {
      ExpertRemove();
      class="kw">return;
     }
   class="type">int r = i + (class="type">int)HistoryBars;
   if(r > bars)
      class="kw">continue;
   for(class="type">int b = class="num">0; b < (class="type">int)HistoryBars; b++)
     {
      class="type">int bar_t = r - b;
      class="type">float open = (class="type">float)Rates[bar_t].open;
      TimeToStruct(Rates[bar_t].time, sTime);
      class="type">float rsi = (class="type">float)RSI.Main(bar_t);
      class="type">float cci = (class="type">float)CCI.Main(bar_t);
      class="type">float atr = (class="type">float)ATR.Main(bar_t);
      class="type">float macd = (class="type">float)MACD.Main(bar_t);
      class="type">float sign = (class="type">float)MACD.Signal(bar_t);
      if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE)
         class="kw">continue;
      class=class="str">"cmt">//---
      if(!State.Add((class="type">float)Rates[bar_t].close - open) || !State.Add((class="type">float)Rates[bar_t].high - open) ||
!State.Add((class="type">float)Rates[bar_t].low - open) || !State.Add((class="type">float)Rates[bar_t].tick_volume / class="num">1000.0f) ||
         !State.Add(sTime.hour) || !State.Add(sTime.day_of_week) || !State.Add(sTime.mon) ||
         !State.Add(rsi) || !State.Add(cci) || !State.Add(atr) || !State.Add(macd) || !State.Add(sign))
         break;
     }
   if(IsStopped())
     {
      ExpertRemove();
      class="kw">return;
     }
   if(State.Total() < (class="type">int)HistoryBars * class="num">12)
      class="kw">continue;
   if(!StudyNet.feedForward(GetPointer(State), class="num">12, true))
     {
      ExpertRemove();
      class="kw">return;
     }

「强化学习回测里的奖惩折算与网络存盘」

这段逻辑跑在每根 K 线回放末尾,先把模型对当前状态的概率分布算出来,再按上一根 K 线的实体涨跌给动作打 reward。action<0 直接卸专家、退出,等于遇到特征缺失就停训,不拿脏数据污染网络。 reward 取 Rates[i-1] 的 close-open,也就是上一根实体幅度。action=0 代表做多倾向,若 reward<0(阴线)则乘 -2 放大惩罚;action=1 做空倾向,阳线乘 -2、阴线乘 -1;其余情况统一取 -fabs(reward) 作最差处置。折扣因子用 pow(DiscountFactor, batch) 衰减,越早期的样本对当前梯度影响越小。 vRewards 先 CumSum 再做最大绝对值归一化,loss 写成 (vRewards * MathLog(vProbs) * -1).Sum(),是典型的策略梯度负对数似然。cum_reward 超过历史 MaxProfit 才把网络存成 .nnw,意味着只保留累计奖励更高的权重快照。 外汇与贵金属杠杆高,这类自训练模型在样本外可能迅速退化,上 MT5 跑前先把 SessionSize 和 DiscountFactor 打印出来核对,避免用默认参直接接实盘。

MQL5 / C++
  StudyNet.getResults(TempData);
  class="type">int action = GetAction(TempData);
  if(action < class="num">0)
    {
     ExpertRemove();
     class="kw">return;
    }
  class="type">class="kw">double reward = Rates[i - class="num">1].close - Rates[i - class="num">1].open;
  class="kw">switch(action)
    {
     case class="num">0:
       if(reward < class="num">0)
         reward *= -class="num">2;
       break;
     case class="num">1:
       if(reward > class="num">0)
         reward *= -class="num">2;
       else
         reward *= -class="num">1;
       break;
     class="kw">default:
       reward = -fabs(reward);
       break;
    }
  if(!States.Add(State))
    {
     ExpertRemove();
     class="kw">return;
    }
  vActions[batch] = (class="type">float)action;
  vRewards[SessionSize - batch - class="num">1] = (class="type">float)(reward * pow(DiscountFactor, (class="type">class="kw">double)batch));
  vProbs[SessionSize - batch - class="num">1] = TempData.At(action);
  class=class="str">"cmt">//---
  }
  class="type">float cum_reward = vRewards.Sum();
vRewards = vRewards.CumSum();
vRewards = vRewards / fmax(vRewards.Max(), fabs(vRewards.Min()));
  class="type">float loss = (vRewards * MathLog(vProbs) * (-class="num">1)).Sum();
  if(MaxProfit < cum_reward)
   {
    if(!StudyNet.Save(FileName + ".nnw", loss, class="num">0, class="num">0, Rates[shift - SessionSize].time, false))
      class="kw">return;
    MaxProfit = cum_reward;
   }
  for(class="type">int batch = class="num">0; batch < SessionSize; batch++)
   {
    State = States.At(batch);
    if(!StudyNet.feedForward(State))
      {
       ExpertRemove();
       class="kw">return;
      }
    if((vRewards[SessionSize - batch - class="num">1] >= class="num">0 ?

常见问题

在 OpenCL kernel 里按输出神经元维度并行算 max 和指数和,再逐元素除以和;先把大数组用 __global 传参,避免每帧拷贝。
用策略梯度把 softmax 输出当动作概率,梯度乘优势函数后回传;折现奖惩按步衰减再归一化,防止量级爆炸。
可以,小布能按你设的均值方差做正态抖动,自动圈出样本窗口并推给训练流程,你只需复核分布。
先从均值 0、方差 1 起步做标准化抖动,再按品种波动缩放;回测里看奖励曲线是否平滑再微调。
存盘时连优化器状态一起写,加载后先用少量样本热启;折算系数要写进元数据,避免重现偏差。