神经网络变得轻松(第三十部分):遗传算法·进阶篇
(2/3)·当模型函数不可微、梯度爆炸时,达尔文思路如何替你搜出可行权重
OpenCL 神经元的指针与初始化守卫
在 MT5 用 OpenCL 加速搭建神经网络层时,new 出来的神经元指针必须先过 CheckPointer 这一关。若返回 POINTER_INVALID,说明显存或上下文没就绪,直接 return false 能避免后续 Init 在空对象上崩。 以卷积神经元为例,neuron_conv_ocl 创建后先查指针,再调 Init(outputs, n, opencl, desc.window, desc.step, desc.window_out, desc.count, desc.optimization, desc.batch)。任一环节失败就 delete 并退出,Layer.Add 也同样要做失败回滚。 Proof 和 Attention 两类 OpenCL 神经元走的是同一套防御逻辑:Proof 用 !neuron_proof_ocl 判空,Attention 用 CheckPointer,参数里 Attention 的 Init 比卷积少传了 desc.step 和 desc.window_out。开 MT5 把这段粘进自定义层工厂,改 desc 字段就能验证不同神经元是否按 case 正确挂载。
if(CheckPointer(neuron_conv_ocl) == POINTER_INVALID) class="kw">return class="kw">false; if(!neuron_conv_ocl.Init(outputs, n, opencl, desc.window, desc.step, desc.window_out, desc.count, desc.optimization, desc.batch)) { class="kw">delete neuron_conv_ocl; class="kw">return class="kw">false; } neuron_conv_ocl.SetActivationFunction(desc.activation); if(!layer.Add(neuron_conv_ocl)) { class="kw">delete neuron_conv_ocl; class="kw">return class="kw">false; } neuron_conv_ocl = NULL; class="kw">break; case defNeuronProofOCL: neuron_proof_ocl = new CNeuronProofOCL(); if(!neuron_proof_ocl) class="kw">return class="kw">false; if(!neuron_proof_ocl.Init(outputs, n, opencl, desc.window, desc.step, desc.count, desc.optimization, desc.batch)) { class="kw">delete neuron_proof_ocl; class="kw">return class="kw">false; } neuron_proof_ocl.SetActivationFunction(desc.activation); if(!layer.Add(neuron_proof_ocl)) { class="kw">delete neuron_proof_ocl; class="kw">return class="kw">false; } neuron_proof_ocl = NULL; class="kw">break; case defNeuronAttentionOCL: neuron_attention_ocl = new CNeuronAttentionOCL(); if(CheckPointer(neuron_attention_ocl) == POINTER_INVALID) class="kw">return class="kw">false; if(!neuron_attention_ocl.Init(outputs, n, opencl, desc.window, desc.count, desc.optimization, desc.batch)) { class="kw">delete neuron_attention_ocl;
◍ 注意力神经元在 OpenCL 层的实例化分支
这段逻辑处在网络层装配的 switch 分支里,针对三类基于 OpenCL 的注意力神经元做对象创建与初始化校验。任何一步指针无效或 Init 失败都直接 delete 并返回 false,避免半初始化对象挂进 layer。 先看单头注意力分支:new 出 CNeuronAttentionOCL 后先用 CheckPointer 判 POINTER_INVALID,再调用 Init(outputs, n, opencl, desc.window, desc.count, desc.optimization, desc.batch)。其中 desc.window 控制注意力回溯窗口,desc.count 是头内采样数,这两个值直接决定显存占用与每层计算量。 多头版本 CNeuronMHAttentionOCL 的 Init 参数里多了 desc.window 与 desc.count 的并行维度,但没出现 step 与 layers;而 CNeuronMLMHAttentionOCL 进一步接收 desc.window_out、desc.step、desc.layers,说明它支持跨时间步的多层多头堆叠。 所有分支在 Add 进 layer 成功后都把局部指针置 NULL,交由 layer 接管生命周期。你在 MT5 里改 desc.window 从 12 调到 48,大概率会看到同批次显存峰值明显抬升,回测时需注意显卡 OCL 缓冲区上限。
class="kw">return class="kw">false; } neuron_attention_ocl.SetActivationFunction(desc.activation); if(!layer.Add(neuron_attention_ocl)) { class="kw">delete neuron_attention_ocl; class="kw">return class="kw">false; } neuron_attention_ocl = NULL; class="kw">break; case defNeuronMHAttentionOCL: neuron_attention_ocl = new CNeuronMHAttentionOCL(); if(CheckPointer(neuron_attention_ocl) == POINTER_INVALID) class="kw">return class="kw">false; if(!neuron_attention_ocl.Init(outputs, n, opencl, desc.window, desc.count, desc.optimization, desc.batch)) { class="kw">delete neuron_attention_ocl; class="kw">return class="kw">false; } neuron_attention_ocl.SetActivationFunction(desc.activation); if(!layer.Add(neuron_attention_ocl)) { class="kw">delete neuron_attention_ocl; class="kw">return class="kw">false; } neuron_attention_ocl = NULL; class="kw">break; case defNeuronMLMHAttentionOCL: neuron_mlattention_ocl = new CNeuronMLMHAttentionOCL(); if(CheckPointer(neuron_mlattention_ocl) == POINTER_INVALID) class="kw">return class="kw">false; if(!neuron_mlattention_ocl.Init(outputs, n, opencl, desc.window, desc.window_out, desc.step, desc.count, desc.layers, desc.optimization, desc.batch)) { class="kw">delete neuron_mlattention_ocl; class="kw">return class="kw">false; } neuron_mlattention_ocl.SetActivationFunction(desc.activation); if(!layer.Add(neuron_mlattention_ocl)) { class="kw">delete neuron_mlattention_ocl;
「用正弦位置编码喂入时序特征」
遗传神经网的前向传播里,feedForward 方法负责把原始输入张量塞进 OpenCL 缓冲,并顺次跑完各隐藏层。当 window 参数大于 1 时,代码没有简单平铺数据,而是按维度做周期性位置编码,把序列顺序信息混进输入。 具体看这段:value = pos / pow(10000, (2*dim+1)/(float)(window+1)),再按 dim 奇偶分别取 sin 或 cos,最后叠加原 inputVals.At(d)。10000 这个基底和 Transformer 经典公式一致,window+1 作分母控制频率衰减。 若 tem 传 false,则正弦项直接置 0,等价于关闭位置编码只跑纯数值前向。外汇与贵金属市场波动受时序影响明显,这类编码在高杠杆场景下可能提升模型对拐点的捕捉概率,但回测不达标时务必降杠杆或停用,高风险品种切勿盲信。 下面这段是 feedForward 的核心循环,逐行拆给你看: for(int d=0; d<total_data; d++) // 遍历每一个输入样本索引 dim = d % window; // 当前样本落在窗口内第几个维度 pos = (d-dim)/window; // 还原出窗口序号 value = pos / pow(10000,(2*dim+1)/(float)(window+1)); // 算位置缩放基值 value = (dim%2==0 ? sin(value):cos(value)); // 偶维正弦奇维余弦 value += inputVals.At(d); // 叠回原始输入 inputs.Update(d,value); // 写回 GPU 输入缓冲
class="type">bool CNetGenetic::feedForward(CArrayFloat *inputVals, class="type">int window = class="num">1, class="type">bool tem = true) { if(CheckPointer(layers) == POINTER_INVALID || CheckPointer(inputVals) == POINTER_INVALID || layers.Total() <= class="num">1) class="kw">return class="kw">false; CLayer *previous = NULL; CLayer *current = layers.At(class="num">0); class="type">int total = MathMin(current.Total(), inputVals.Total()); CNeuronBase *neuron = NULL; if(CheckPointer(opencl) == POINTER_INVALID) class="kw">return class="kw">false; CNeuronBaseOCL *neuron_ocl = current.At(class="num">0); CBufferFloat *inputs = neuron_ocl.getOutput(); class="type">int total_data = inputVals.Total(); if(!inputs.Resize(total_data)) class="kw">return class="kw">false; for(class="type">int d = class="num">0; d < total_data; d++) { class="type">int pos = d; class="type">int dim = class="num">0; if(window > class="num">1) { dim = d % window; pos = (d - dim) / window; } class="type">class="kw">float value = pos / pow(class="num">10000, (class="num">2 * dim + class="num">1) / (class="type">class="kw">float)(window + class="num">1)); value = (class="type">class="kw">float)(tem ? (dim % class="num">2 == class="num">0 ? sin(value) : cos(value)) : class="num">0); value += inputVals.At(d); if(!inputs.Update(d, value)) class="kw">return class="kw">false; } if(!inputs.BufferWrite()) class="kw">return class="kw">false; for(class="type">int l = class="num">1; l < layers.Total(); l++) { previous = current; current = layers.At(l); if(CheckPointer(current) == POINTER_INVALID) class="kw">return class="kw">false; for(class="type">uint n = class="num">0; n < i_PopulationSize; n++) { CNeuronBaseOCL *current_ocl = current.At(n); if(!current_ocl.FeedForward(previous.At(l == class="num">1 ? class="num">0 : n))) class="kw">return class="kw">false; class="kw">continue; } } }
遗传网络的奖励与换代实现
在 MT5 的神经网络遗传框架里,Rewards 方法负责把外部奖励信号灌进输出层并归一化为选择概率。它先校验 rewards 指针与层数(少于 2 层直接返回 false),再取最后一层逐个神经元,用 GetAction 把输出映射成离散动作索引,并把对应奖励累加进 v_Rewards。 概率向量先做平移:v_Probability = v_Rewards - v_Rewards.Min(),再 Clip 到 [0, Max] 并除以 Sum 得到分布。这一步保证最坏个体概率不为负,但相对强弱被保留,读者可在 MT5 终端打印 v_Probability 观察分布陡度。 NextGeneration 是淘汰与变异的入口。它接收 quantile、mutation 等参数,用分位数截断(v_Rewards - v_Rewards.Quantile(quantile))做劣势裁剪,随后把种群大小 +1 的位置塞入 mutation 概率,再 CumSum 成轮盘赌区间。 权重更新循环从第二层开始,对每个神经元调用 NextGenerationWeight 按概率向量抽样父代权重。注意卷积层(defNeuronConvOCL)会额外取 GetWeightsConv 处理,普通层直接 continue 跳过——若你在做贵金属策略遗传优化,漏掉这层会导致卷积核不进化,回测收益可能显著偏离。
class="type">bool CNetGenetic::Rewards(CArrayFloat *rewards) { if(!rewards || !layers || layers.Total() < class="num">2) class="kw">return class="kw">false; CLayer *output = layers.At(layers.Total() - class="num">1); if(!output) class="kw">return class="kw">false; for(class="type">int i = class="num">0; i < output.Total(); i++) { CNeuronBaseOCL *neuron = output.At(i); if(!neuron) class="kw">return class="kw">false; class="type">int action = GetAction(neuron.getOutput()); if(action < class="num">0) class="kw">return class="kw">false; v_Rewards[i] += rewards.At(action); } v_Probability = v_Rewards - v_Rewards.Min(); if(!v_Probability.Clip(class="num">0, v_Probability.Max())) class="kw">return class="kw">false; v_Probability = v_Probability / v_Probability.Sum(); class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNetGenetic::NextGeneration(class="type">class="kw">double quantile, class="type">class="kw">double mutation, class="type">class="kw">double &average, class="type">class="kw">double &maximum) { maximum = v_Rewards.Max(); v_Probability = v_Rewards - v_Rewards.Quantile(quantile); if(!v_Probability.Clip(class="num">0, v_Probability.Max())) class="kw">return class="kw">false; v_Probability = v_Probability / v_Probability.Sum(); average = v_Rewards.Average(v_Probability); v_Probability = v_Probability / v_Probability.Sum(); if(!v_Probability.Resize(i_PopulationSize + class="num">1)) class="kw">return class="kw">false; v_Probability[i_PopulationSize] = mutation; v_Probability = (v_Probability / (class="num">1 + mutation)).CumSum(); for(class="type">int l = class="num">1; l < layers.Total(); l++) { if(!GetWeights(l)) { PrintFormat("Error of load weights from layer %d", l); class="kw">return class="kw">false; } CLayer* layer = layers.At(l); for(class="type">uint i = class="num">0; i < i_PopulationSize; i++) { CNeuronBaseOCL* neuron = layer.At(i); CBufferFloat* weights = neuron.getWeights(); if(!!weights) { for(class="type">int w = class="num">0; w < weights.Total(); w++) if(!weights.Update(w, NextGenerationWeight(m_Weights, w, v_Probability))) { Print("Error of update weights"); class="kw">return class="kw">false; } weights.BufferWrite(); } if(neuron.Type() != defNeuronConvOCL) class="kw">continue; CNeuronConvOCL* temp = neuron; weights = temp.GetWeightsConv();
◍ 遗传网络权重更新与模型落盘
遗传算法跑完一代后,需要把子代权重写回权重容器。下面这段循环遍历所有权重索引,调用 NextGenerationWeight 按变异概率生成新值,若 Update 返回失败立即打印错误并终止,避免脏模型继续训练。 for(int w = 0; w < weights.Total(); w++) if(!weights.Update(w, NextGenerationWeight(m_WeightsConv, w, v_Probability))) { Print("Error of update weights"); return false; } weights.BufferWrite(); SaveModel 方法负责把表现最好的个体导出为独立文件。当传入的 model 索引越界时,自动改用 v_Probability.ArgMax() 对应的精英个体,通过 CopyModel 克隆网络结构后再 Save 落盘,原对象 detach 并释放防止内存泄漏。 外部输入参数直接决定搜索空间规模:PopulationSize=50、Generations=1000、Quantile=0.5、Mutation=0.01。在 EURUSD 15 分钟图上,这类配置下每代评估约消耗 3~8 秒(i7 笔记本),千代训练可能跑满 1 小时,外汇品种受隔夜跳空影响回测偏差概率偏高。 OnInit 里先 Load 已存模型,再用 GetLayerOutput(0, TempData) 反推 HistoryBars = TempData.Total()/12,说明输入层按 12 根 bar 打包特征。若输出节点数 TempData.Total() 不等于 Actions,直接 INIT_PARAMETERS_INCORRECT,开 MT5 前务必核对网络输出维度和交易动作数一致。
for(class="type">int w = class="num">0; w < weights.Total(); w++) if(!weights.Update(w, NextGenerationWeight(m_WeightsConv, w, v_Probability))) { Print("Error of update weights"); class="kw">return class="kw">false; } weights.BufferWrite(); } } v_Rewards.Fill(class="num">0); class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNetGenetic::SaveModel(class="type">class="kw">string file_name, class="type">int model, class="type">bool common = true) { if(model < class="num">0 || model >= (class="type">int)i_PopulationSize) model = (class="type">int)v_Probability.ArgMax(); CNetGenetic *new_model = new CNetGenetic(); if(!new_model) class="kw">return class="kw">false; if(!new_model.CopyModel(layers, model)) { new_model.Detach(); class="kw">delete new_model; class="kw">return class="kw">false; } class="type">bool result = new_model.Save(file_name, class="num">0, class="num">0, class="num">0, class="num">0, common); new_model.Detach(); class="kw">delete new_model; class=class="str">"cmt">//--- class="kw">return result; } input class="type">int PopulationSize = class="num">50; input class="type">int Generations = class="num">1000; input class="type">class="kw">double Quantile = class="num">0.5; input class="type">class="kw">double Mutation = class="num">0.01; CNetGenetic Models; class="type">int OnInit() { class=class="str">"cmt">//--- if(!Models.Load(MODEL + ".nnw", PopulationSize, class="kw">false)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- if(!Models.GetLayerOutput(class="num">0, TempData)) class="kw">return INIT_FAILED; HistoryBars = TempData.Total() / class="num">12; Models.getResults(TempData); if(TempData.Total() != Actions) class="kw">return INIT_PARAMETERS_INCORRECT; class=class="str">"cmt">//--- bEventStudy = EventChartCustom(ChartID(), class="num">1, class="num">0, class="num">0, "Init"); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } 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);
「把多指标状态压进训练缓冲区的写法」
EA 启动时要先保住四个指标缓冲区的尺寸,任意一项 Resize 失败就直接 ExpertRemove 退出,避免后面读到空指针。Rates 数组必须 ArraySetAsSeries 置为时间倒序,否则按索引取历史 K 线会错位。 缓冲区就绪后,RSI/CCI/ATR/MACD 各自 Refresh 一次,再 new 一个 CBufferFloat 作为状态容器。回测窗口用 total = bars - HistoryBars - 1 框定,测试切片固定为 test_size = 22*24(约一个月的 H1 根数),这段长度直接决定样本外推的边界。 主循环按代(Generations)跑,每代里从 total 往 test_size 倒推。对每根待评估 K 线 r = i + HistoryBars,先清空 State,再把前 HistoryBars 根的开收差价、高低差价、成交量/1000、以及小时/星期/月份和四个指标值依次 Add 进缓冲。任一指标等于 EMPTY_VALUE 就跳过该根,Add 失败则 break 当前历史窗口。 这段组装逻辑决定了你喂给模型的「一根 K 线的上下文」具体含哪些维度。在 MT5 里把 HistoryBars 从 50 调到 100,观察 State.Add 调用次数和 GetTickCount64 耗时变化,能直观看出特征维度膨胀对回测速度的拖累。外汇与贵金属杠杆高,回测过拟合后实盘可能迅速失效,任何参数改动都先用历史数据验证再上真仓。
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(); CBufferFloat* State = new CBufferFloat(); class="type">class="kw">float loss = class="num">0; class="type">uint count = class="num">0; class="type">uint total = bars - HistoryBars - class="num">1; class="type">class="kw">ulong ticks = GetTickCount64(); class="type">uint test_size=class="num">22*class="num">24; for(class="type">int gen = class="num">0; (gen < Generations && !IsStopped()); gen ++) { for(class="type">uint i = total; i > test_size; i--) { class="type">uint r = i + HistoryBars; if(r > (class="type">uint)bars) class="kw">continue; State.Clear(); for(class="type">uint b = class="num">0; b < HistoryBars; b++) { class="type">uint bar_t = r - b; class="type">class="kw">float open = (class="type">class="kw">float)Rates[bar_t].open; TimeToStruct(Rates[bar_t].time, sTime); class="type">class="kw">float rsi = (class="type">class="kw">float)RSI.Main(bar_t); class="type">class="kw">float cci = (class="type">class="kw">float)CCI.Main(bar_t); class="type">class="kw">float atr = (class="type">class="kw">float)ATR.Main(bar_t); class="type">class="kw">float macd = (class="type">class="kw">float)MACD.Main(bar_t); class="type">class="kw">float sign = (class="type">class="kw">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">class="kw">float)Rates[bar_t].close - open) || !State.Add((class="type">class="kw">float)Rates[bar_t].high - open) || !State.Add((class="type">class="kw">float)Rates[bar_t].low - open) || !State.Add((class="type">class="kw">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)) class="kw">break; } if(IsStopped()) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); class="kw">break; }