神经网络变得轻松(第三十一部分):进化算法·进阶篇
(2/3)· 当模型不可导或求导成本过高,如何用 MQL5 把遗传随机性与梯度方向拧成一条优化路径
「进化网络的代际繁衍逻辑」
在 MT5 的神经网络遗传框架里,CNetEvolution 把「种群生成」和「变异推进」绑在了一个类里。Create 方法先调父类 CNetGenetic::Create 建种群,随即以 mutation=0 跑一次 NextGeneration,相当于先拿到第 0 代的平均奖励与最大奖励基线。 NextGeneration 的开头两行直接取 v_Rewards.Max() 和 v_Rewards.Mean() 填进引用参数 maximum、average,这两个值就是你在 EA 日志里观察收敛速度的硬指标。mutation 被 MathMin 截断到 MaxMutation 上限,避免一次变异过猛把权重打散。 概率分布的计算很直接:v_Probability = v_Rewards - 均值,再除以绝对值之和做归一。若 Sum==0(全体奖励完全相同),就把第 0 位概率强行置 1,防止后面抽样崩掉。 权重变异循环里,对每个 neuron 的 weights 缓冲区逐元素处理:用 MathRandomNormal(0.5, 0.5) 抽一个正态随机数,仅当 mutation > random 时才对该权重做扰动。这意味着 mutation 参数本质是个「变异触发概率」,调大它后代离散度倾向升高,但过度调大可能让外汇/贵金属模型在样本外表现不稳定,属高风险操作。 [CODE] virtual bool NextGeneration(float mutation, float &average, float &mamximum); virtual bool Load(string file_name, uint population_size, bool common = true) override; virtual bool Save(string file_name, bool common = true); //--- virtual bool GetLayerOutput(uint layer, CBufferFloat *&result) override; virtual void getResults(CBufferFloat *&resultVals); }; bool CNetEvolution::Create(CArrayObj *Description, uint population_size) { if(!CNetGenetic::Create(Description, population_size)) return false; float average, maximum; return NextGeneration(0,average, maximum); } bool CNetEvolution::NextGeneration(float mutation, float &average, float &maximum) { maximum = v_Rewards.Max(); average = v_Rewards.Mean(); mutation = MathMin(mutation, MaxMutation); v_Probability = v_Rewards - v_Rewards.Mean(); float Sum = MathAbs(v_Probability).Sum(); if(Sum == 0) v_Probability[0] = 1; else v_Probability = v_Probability / Sum; for(int l = 1; l < layers.Total(); l++) { CLayer *layer = layers.At(l); if(!layer) return false; if(layer.Total() < (int)i_PopulationSize) if(!CreatePopulation()) return false; if(!GetWeights(l)) return false; for(uint i = 0; i < i_PopulationSize; i++) { CNeuronBaseOCL* neuron = layer.At(i); if(!neuron) return false; CBufferFloat* weights = neuron.getWeights(); if(!!weights) { for(int w = 0; w < weights.Total(); w++) { if(mutation > 0) { int err_code; float random = (float)Math::MathRandomNormal(0.5, 0.5, err_code); if(mutation > random) { [/CODE]
class="kw">virtual class="type">bool NextGeneration(class="type">class="kw">float mutation, class="type">class="kw">float &average, class="type">class="kw">float &mamximum); class="kw">virtual class="type">bool Load(class="type">class="kw">string file_name, class="type">uint population_size, class="type">bool common = true) class="kw">override; class="kw">virtual class="type">bool Save(class="type">class="kw">string file_name, class="type">bool common = true); class=class="str">"cmt">//--- class="kw">virtual class="type">bool GetLayerOutput(class="type">uint layer, CBufferFloat *&result) class="kw">override; class="kw">virtual class="type">void getResults(CBufferFloat *&resultVals); }; class="type">bool CNetEvolution::Create(CArrayObj *Description, class="type">uint population_size) { if(!CNetGenetic::Create(Description, population_size)) class="kw">return class="kw">false; class="type">class="kw">float average, maximum; class="kw">return NextGeneration(class="num">0,average, maximum); } class="type">bool CNetEvolution::NextGeneration(class="type">class="kw">float mutation, class="type">class="kw">float &average, class="type">class="kw">float &maximum) { maximum = v_Rewards.Max(); average = v_Rewards.Mean(); mutation = MathMin(mutation, MaxMutation); v_Probability = v_Rewards - v_Rewards.Mean(); class="type">class="kw">float Sum = MathAbs(v_Probability).Sum(); if(Sum == class="num">0) v_Probability[class="num">0] = class="num">1; else v_Probability = v_Probability / Sum; for(class="type">int l = class="num">1; l < layers.Total(); l++) { CLayer *layer = layers.At(l); if(!layer) class="kw">return class="kw">false; if(layer.Total() < (class="type">int)i_PopulationSize) if(!CreatePopulation()) class="kw">return class="kw">false; if(!GetWeights(l)) class="kw">return class="kw">false; for(class="type">uint i = class="num">0; i < i_PopulationSize; i++) { CNeuronBaseOCL* neuron = layer.At(i); if(!neuron) class="kw">return class="kw">false; CBufferFloat* weights = neuron.getWeights(); if(!!weights) { for(class="type">int w = class="num">0; w < weights.Total(); w++) { if(mutation > class="num">0) { class="type">int err_code; class="type">class="kw">float random = (class="type">class="kw">float)Math::MathRandomNormal(class="num">0.5, class="num">0.5, err_code); if(mutation > random) {
卷积层权重变异的随机触发逻辑
在神经网络权重更新环节,若神经元类型不是 defNeuronConvOCL 则直接跳过,只对卷积类神经元做权重变异处理。 遍历卷积权重容器时,先判断 mutation 变量是否大于 0,只有开启变异概率才进入后续随机逻辑。 核心随机量由 MathRandomNormal(0.5, 0.5, err_code) 生成,均值 0.5、标准差 0.5 的正态分布浮点数,当 mutation 大于该随机数时才真正替换权重。 若 Update 写入失败会打印 "Error updating the weights" 并返回 false,调用方需捕获该错误避免训练中断。 在 MT5 里把 mutation 从 0.1 调到 0.3 观察权重更新频率变化,能直观看到变异强度对模型收敛的可能影响,外汇与贵金属模型训练属高风险实验。
if(neuron.Type() != defNeuronConvOCL) class="kw">continue; CNeuronConvOCL* temp = neuron; weights = temp.GetWeightsConv(); for(class="type">int w = class="num">0; w < weights.Total(); w++) { if(mutation > class="num">0) { class="type">int err_code; class="type">class="kw">float random = (class="type">class="kw">float)Math::MathRandomNormal(class="num">0.5, class="num">0.5, err_code); if(mutation > random) { if(!weights.Update(w, GenerateWeight((class="type">uint)m_WeightsConv.Cols()))) { Print("Error updating the weights");
◍ 权重更新里的均值归零与概率加权
在 CNetEvolution::GetWeights 里,先判断 v_Probability.Sum() 是否为 0,若为 0 直接返回 false,避免后续用空概率向量做加权。接着调基类 CNetGenetic::GetWeights(layer) 取原始权重,失败同样返回 false。 对全连接权重 m_Weights,若列数大于 0,先算每列均值 mean = m_Weights.Mean(0),再用 Zeros(1, Cols) 构造单行矩阵并写入均值,通过 Ones(Rows,1) 外积铺成与权重同形的偏移矩阵,执行 m_Weights = m_Weights - temp 完成去均值(中心化)。 中心化后做概率加权更新:mean = mean + m_Weights.Transpose().MatMul(v_Probability) * lr,这里 lr 是学习率,权重沿概率梯度方向偏移。随后 Resize(1, Cols) 并把 mean 写回第 0 行。卷积权重 m_WeightsConv 走完全一致的流程,只是矩阵对象换成 m_WeightsConv。 实盘接这套逻辑时,lr 若设 0.01 可能收敛慢,设 0.5 可能震荡,建议在 MT5 策略测试器里用 EURUSD 的 M15 历史数据跑 3 个月窗口观察权重方差变化再定。外汇与贵金属杠杆高,回测稳健不等于实盘能复制,参数敏感度高。
class="type">bool CNetEvolution::GetWeights(class="type">uint layer) { if(v_Probability.Sum() == class="num">0) class="kw">return class="kw">false; if(!CNetGenetic::GetWeights(layer)) class="kw">return class="kw">false; if(m_Weights.Cols() > class="num">0) { vectorf mean = m_Weights.Mean(class="num">0); matrixf temp = matrixf::Zeros(class="num">1, m_Weights.Cols()); if(!temp.Row(mean, class="num">0)) class="kw">return class="kw">false; temp = (matrixf::Ones(m_Weights.Rows(), class="num">1)).MatMul(temp); m_Weights = m_Weights - temp; mean = mean + m_Weights.Transpose().MatMul(v_Probability) * lr; if(!m_Weights.Resize(class="num">1, m_Weights.Cols())) class="kw">return class="kw">false; if(!m_Weights.Row(mean, class="num">0)) class="kw">return class="kw">false; } if(m_WeightsConv.Cols() > class="num">0) { vectorf mean = m_WeightsConv.Mean(class="num">0); matrixf temp = matrixf::Zeros(class="num">1, m_WeightsConv.Cols()); if(!temp.Row(mean, class="num">0)) class="kw">return class="kw">false; temp = (matrixf::Ones(m_WeightsConv.Rows(), class="num">1)).MatMul(temp); m_WeightsConv = m_WeightsConv - temp; mean = mean + m_WeightsConv.Transpose().MatMul(v_Probability) * lr;
「进化网络的奖励归一与模型存取」
CNetEvolution 把遗传算法的奖励向量做中心化后再按绝对值总和归一,得到每个个体的被选概率。Rewards 方法里先调基类写回奖励,随后 v_Probability = v_Rewards - v_Rewards.Mean() 去掉均值,再除以 MathAbs(v_Probability).Sum() 完成缩放,这一步直接决定下一代采样权重。 Load 函数在载入模型后会把 v_Rewards 清零,并立即以 NextGeneration(0, average, maximum) 跑一代初始化;若 Sum 为 0,NextGeneration 里会把 v_Probability[0] 强行置 1,避免除零导致全盘失效。 Save 与 Load 都走 CNetGenetic 的模型文件接口,common 参数默认 true 表示存到公共目录。外汇与贵金属行情下用这类网络做信号演化,参数过拟合概率偏高,实盘前务必在 MT5 策略测试器用多品种回测验证。
if(!m_WeightsConv.Resize(class="num">1, m_WeightsConv.Cols())) class="kw">return class="kw">false; if(!m_WeightsConv.Row(mean, class="num">0)) class="kw">return class="kw">false; } class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNetEvolution::SetPopulationSize(class="type">uint size) { class="kw">return CNetGenetic::SetPopulationSize(size); } class="type">bool CNetEvolution::feedForward(CArrayFloat *inputVals, class="type">int window = class="num">1, class="type">bool tem = true) { class="kw">return CNetGenetic::feedForward(inputVals, window, tem); } class="type">bool CNetEvolution::Rewards(CArrayFloat *rewards) { if(!CNetGenetic::Rewards(rewards)) class="kw">return class="kw">false; class=class="str">"cmt">//--- v_Probability = v_Rewards - v_Rewards.Mean(); v_Probability = v_Probability / MathAbs(v_Probability).Sum(); class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNetEvolution::GetLayerOutput(class="type">uint layer, CBufferFloat *&result) { class="kw">return CNet::GetLayerOutput(layer, result); } class="type">void CNetEvolution::getResults(CBufferFloat *&resultVals) { CNetGenetic::getResults(resultVals); } class="type">bool CNetEvolution::Save(class="type">class="kw">string file_name, class="type">bool common = true) { class="kw">return CNetGenetic::SaveModel(file_name, -class="num">1, common); } class="type">bool CNetEvolution::Load(class="type">class="kw">string file_name, class="type">uint population_size, class="type">bool common = true) { if(!CNetGenetic::Load(file_name, population_size, common)) class="kw">return class="kw">false; v_Rewards.Fill(class="num">0); class="type">class="kw">float average, maximum; if(!NextGeneration(class="num">0, average, maximum)) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNetEvolution::NextGeneration(class="type">class="kw">float mutation, class="type">class="kw">float &average, class="type">class="kw">float &maximum) { ............. ............. ............. v_Probability = v_Rewards - v_Rewards.Mean(); class="type">class="kw">float Sum = MathAbs(v_Probability).Sum(); if(Sum == class="num">0) v_Probability[class="num">0] = class="num">1; else v_Probability = v_Probability / Sum; ............. ............. ............. }