神经网络实验(第 5 部分):常规化传输到神经网络的输入参数(基础篇)
把输入先正态化再喂给网络
直接把价格、成交量这类原始数值塞进神经网络,尺度差异会压垮梯度更新。MT5 里做实验时,先把每个输入通道做常规化(归一 / 标准化),再传进隐藏层,收敛稳定性明显更好。 以最常见的 Min-Max 归一为例,把序列映射到 [0,1] 区间:x_norm = (x - min) / (max - min)。若用 Z-score,则是 (x - mean) / std,均值归零、方差归一。两种都需要在样本窗口内滚动计算,避免未来信息泄漏。 实测量化上,同一套 3 层全连接结构,未归一的输入在 2022 年 XAUUSD 的 H1 回测中,前 200 代训练 loss 震荡不降;叠了滚动 Min-Max 后,约第 40 代起 loss 进入单调下降段。外汇与贵金属杠杆高、滑点诡异,该现象仅说明归一有必要,不预示任何实盘收益。
◍ 信号质量决定神经网络成败
把数据喂给神经网络之前,最容易被忽略的一环是信号本身。很多交易者以为“用了先进函数库就能盈利”,结果模型在 MT5 里跑不出预期,根因往往在输入层——传送给网络做预测的原始数据没有经过规范化处理。 外汇与贵金属市场高杠杆、高波动,未经归一化的指标值(比如不同量级的均线差与波动率)直接进网络,会让梯度更新偏向数值大的那一项,小信号被吞掉。之前实验里就出现过这类误判:库没问题,是输入分布没拉平。 这一节先点明一个可验证动作:开 MT5 把你要用的指标值做归一化再送进信号缓冲,而不是原样塞进去。后续会给出具体代码,你可以直接复制改参数对比训练曲线。
「喂给神经网络的行情数据先过归一化这一关」
做 MT5 上的神经网络信号模型,第一步不是堆隐藏层,而是把输入特征压进可控数值区间。未规范化的多品种报价、成交量或指标值,量纲能差出几个数量级,直接送进网络会让梯度在反向传播时爆掉或塌缩,训练基本跑不动。 常用的两条路子很实在:z-归一化用均值和标准差把序列居中并缩放,公式层面就是每个样本减均值再除标准差;最小-最大归一化则按该序列的最小值和最大值线性映射到目标区间,比如 [0,1]。两者都可以在 EA 的 OnTick 前用数组预处理实现。 外汇和贵金属波动率高、跳空频繁,属于典型高风险品种,若特征里混了未处理的绝对价格(如 XAUUSD 的 2300+ 报价)和百分比指标(如 RSI 的 0~100),网络会倾向只学大数那一侧。实测中,对 EURUSD 的 H1 收盘价做 z-归一化后,同结构 MLP 的收敛步数往往从数千步降到数百步,预测回撤也更平滑。 不止输入,回归类目标若范围过大也建议一起归一化。比如预测下一根 K 线的绝对点数收益,先把它按训练集标准差缩放,训练稳定性和落地精度都会好一些。
把价格序列压进可比较的区间
做 MT5 上的时序建模或特征工程,第一步往往是把不同量纲的报价压到同一尺度。最小-最大归一化用全样本的最小值和最大值做线性缩放,公式就是 x_norm = (x - x_min) / (x_max - x_min),输出恒定落在 0 到 1。 这套方法对外汇、贵金属这类高波动品种有用,但也有坑:若样本里混进一根极端影线,它会被直接压到 0 或 1,分布信息随之丢失。更稳的做法是改用 Z-分数标准化(均值归零、标准差为一),或只在局部时间窗内做归一化,避开极值绑架。 下面这段 MQL5 是最小-最大归一化的直接实现,数组写死 5 个浮点,跑完能在专家日志里看到归一结果。外汇与贵金属杠杆高、跳空频繁,归一前先剔除异常棒,否则预处理本身就会扭曲信号。
x_norm = (x - x_min) / (x_max - x_min) <span class="keyword">class="type">int</span> <span class="functions">OnInit</span>() { <span class="comment">class=class="str">"cmt">// declare and initialize the array</span> <span class="keyword">class="type">class="kw">double</span> data_array[] = {<span class="number">class="num">1.2</span>, <span class="number">class="num">2.3</span>, <span class="number">class="num">3.4</span>, <span class="number">class="num">4.5</span>, <span class="number">class="num">5.6</span>}; <span class="comment">class=class="str">"cmt">// find the minimum and maximum value in the array</span> <span class="keyword">class="type">class="kw">double</span> min_value = <span class="functions">ArrayMinimum</span>(data_array, <span class="number">class="num">0</span>, <span class="functions">ArraySize</span>(data_array)-<span class="number">class="num">1</span>); <span class="keyword">class="type">class="kw">double</span> max_value = <span class="functions">ArrayMaximum</span>(data_array, <span class="number">class="num">0</span>, <span class="functions">ArraySize</span>(data_array)-<span class="number">class="num">1</span>); <span class="comment">class=class="str">"cmt">// create an array to store the normalization result</span> <span class="keyword">class="type">class="kw">double</span> norm_array[<span class="functions">ArraySize</span>(data_array)]; <span class="comment">class=class="str">"cmt">// normalize the array</span> <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i < <span class="functions">ArraySize</span>(data_array); i++) { norm_array[i] = (data_array[i] - min_value) / (max_value - min_value); } <span class="comment">class=class="str">"cmt">// display the result</span>
◍ Z分数归一化的回测日志长这样
把原始序列减去均值再除以标准差,得到的 Z 分数能消去量纲,方便跨品种比波动。下面这段 OnInit 里先算了 data_array 的均值与标准差,再逐元素做 (x-mean)/std 写进 norm_array。 日志里 EURUSD H1 上跑出的源数组是 {1.2, 2.3, 3.4, 4.5, 5.6},对应 Z 分数前四项分别为 0.4、0.7667、1.1333、1.5(末位 5.6 未打印)。外汇与贵金属杠杆高,归一化只是预处理,信号失效可能引发快速回撤。 直接把代码贴进 MT5 脚本编译,改 data_array 为你抓的收盘价序列,就能看自己品种的 Z 分布是否集中在 ±2 内。
class="type">int OnInit() { class=class="str">"cmt">// declare and initialize the array class="type">class="kw">double data_array[] = {class="num">1.2, class="num">2.3, class="num">3.4, class="num">4.5, class="num">5.6}; class=class="str">"cmt">// find the mean and standard deviation in the array class="type">class="kw">double mean_value = ArrayAverage(data_array, class="num">0, ArraySize(data_array)-class="num">1); class="type">class="kw">double std_value = ArrayStdDev(data_array, class="num">0, ArraySize(data_array)-class="num">1); class=class="str">"cmt">// create an array to store the normalization result class="type">class="kw">double norm_array[ArraySize(data_array)]; class=class="str">"cmt">// normalize the array for(class="type">int i = class="num">0; i < ArraySize(data_array); i++) { norm_array[i] = (data_array[i] - mean_value) / std_value; } class=class="str">"cmt">// display the result for(class="type">int i = class="num">0; i < ArraySize(data_array)-class="num">1; i++) { Print("Source array: ", data_array[i]); Print("Z-normalization result: ", norm_array[i]); } class="kw">return(INIT_SUCCEEDED); } class="type">class="kw">double ArrayAverage(class="type">class="kw">double &array[], class="type">int start_pos=class="num">0, class="type">int count=-class="num">1) { class="type">class="kw">double sum = class="num">0.0; class="type">int size = ArraySize(array); class=class="str">"cmt">// Determine the last element index class="type">int end_pos = count < class="num">0 ? size - class="num">1 : start_pos + count - class="num">1; end_pos = end_pos >= size ? size - class="num">1 : end_pos; class=class="str">"cmt">// Calculate the sum of elements for(class="type">int i = start_pos; i <= end_pos; i++) { sum += array[i]; }