神经网络变得轻松(第二十八部分):政策梯度算法·进阶篇
用 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 把政策梯度模型跑起来训练。
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 偏小,概率分布会略有偏移,属高风险实验环境。
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 线,意味着开头段永远不参与迭代。
__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,采样耗时可能翻倍。
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 打印出来核对,避免用默认参直接接实盘。
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 ?