使用 Python 的深度学习 GRU 模型到使用 EA 的 ONNX,以及 GRU 与 LSTM 模型的比较·综合运用
📘

使用 Python 的深度学习 GRU 模型到使用 EA 的 ONNX,以及 GRU 与 LSTM 模型的比较·综合运用

第 3/3 篇

编译模型时损失与优化器的分工

网络搭好只是空壳,权重初始随机意味着它此时对任何价格规律都一无所知。要让密集层真正学会把输入特征映射到目标输出,必须靠训练数据反推权重,而训练的两个指挥棒是损失函数和优化器。 损失函数量化预测值与真实值的差距。回归任务里平均绝对误差(MAE)算的是 abs(y_true - y_pred) 的均值,衡量拟合曲线到真实点的平均距离;均方误差(MSE)和 Huber 损失也常用,Keras 里都能直接调。模型把损失当导航,越低说明权重越贴近训练集里特征与目标的真实关系。 优化器负责按损失反馈去微调权重。主流都是随机梯度下降(SGD)类:每步抽一小批数据前向出预测、算损失、沿降损方向挪权重。每批叫 batch,扫一遍全数据叫一个 epoch。学习率决定每步挪多大,批量大小决定梯度噪声水平,两者咬合不好训练会抖或卡住。 Adam 带了自适应学习率,不用手动搜超参就能在大多数任务上跑出像样结果,是省事的通用选项。下面这段用 adam + MSE 编译并训练,batch_size 设 256、验证集拆 0.2,第一轮训练损失 0.0021、val_loss 8.1e-4,到第 7 轮损失降到 2.2985e-4 附近,说明权重在收敛但验证损失仍在 7~9e-4 波动,过拟合风险不能忽视。外汇与贵金属行情高波动,拿这类模型实盘前务必用跨周期样本复查。

MQL5 / C++
# Compile the model
model.compile(optimizer=&class="macro">#x27;adam&class="macro">#x27;, loss=&class="macro">#x27;mean_squared_error&class="macro">#x27;)
model.compile(optimizer=&class="macro">#x27;adam&class="macro">#x27;, loss=&class="macro">#x27;mean_squared_error&class="macro">#x27;)
# Train the model
model.fit(X_train_scaled, y_train, epochs=class="type">int(epoch), batch_size=class="num">256, validation_split=class="num">0.2, verbose=class="num">1)
class="num">44241/class="num">44241 [==============================] - 247s 6ms/step - loss: class="num">0.0021 - val_loss: class="num">8.0975e-04
Epoch class="num">2/class="num">30
class="num">44241/class="num">44241 [==============================] - 247s 6ms/step - loss: class="num">2.3062e-04 - val_loss: class="num">0.0010
Epoch class="num">3/class="num">30
class="num">44241/class="num">44241 [==============================] - 288s 7ms/step - loss: class="num">2.3019e-04 - val_loss: class="num">8.5903e-04
Epoch class="num">4/class="num">30
class="num">44241/class="num">44241 [==============================] - 248s 6ms/step - loss: class="num">2.3003e-04 - val_loss: class="num">7.6378e-04
Epoch class="num">5/class="num">30
class="num">44241/class="num">44241 [==============================] - 257s 6ms/step - loss: class="num">2.2993e-04 - val_loss: class="num">9.5630e-04
Epoch class="num">6/class="num">30
class="num">44241/class="num">44241 [==============================] - 247s 6ms/step - loss: class="num">2.2988e-04 - val_loss: class="num">7.3110e-04
Epoch class="num">7/class="num">30
class="num">44241/class="num">44241 [==============================] - 224s 5ms/step - loss: class="num">2.2985e-04 - val_loss: class="num">8.7191e-04

「从信号噪声权衡看训练终止点」

训练集和验证集的损失曲线,本质是信号与噪声的分离计。训练数据里总混着两部分:能泛化到新样本的真实结构(信号),以及随机波动和无意义模式(噪声)。模型只要更新权重,训练损失就会降;但验证损失只在学到信号时才降,吞下噪声只会让两条曲线拉开缺口,缺口宽度就是过噪声的程度。 现实里做不到只学信号不学噪声,只能是权衡。容量够低时信号都吃不透,训练损失下不去,这是欠拟合;容量过高或训太久,噪声被当真结构记下来,验证损失拐头向上,就是过度拟合。外汇与贵金属特征里噪声极多,这类失衡会让样本外推断概率明显恶化,属高风险环节。 早期停止是直接掐断过拟合的手段:监控验证损失,连续若干轮没改善就停,并把权重回滚到最优轮次。Keras 用回调挂这个逻辑,下面代码里 patience=20、min_delta=0.001 的意思是「连续 20 个 epoch 验证损失提升不足 0.001 就停,且恢复最佳权重」,初始 epoch 设 300 只是给早停留余地,实际可能在几十轮就终止。 别把验证损失微升当立刻过拟合 随机 batch 差异也会让验证损失抖一下,所以靠 min_delta 设容忍阈,避免被单轮波动误杀;同时容量不够时早停会卡在欠拟合区,加层或加单元后再看曲线才公平。

MQL5 / C++
from tensorflow class="kw">import keras
from tensorflow.keras class="kw">import layers, callbacks
early_stopping = callbacks.EarlyStopping(
    min_delta=class="num">0.001, # minimium amount of change to count as an improvement
    patience=class="num">20, # how many epochs to wait before stopping
    restore_best_weights=True,
)
# Train the model
model.fit(X_train_scaled, y_train, epochs=class="type">int(epoch), batch_size=class="num">256, validation_split=class="num">0.2,callbacks=[early_stopping], verbose=class="num">1)
model.add(Dense(class="num">128, activation=&class="macro">#x27;relu&class="macro">#x27;, input_shape=(X_train.shape[class="num">1],), kernel_regularizer=l2(k_reg)))
model.add(Dense(class="num">256, activation=&class="macro">#x27;relu&class="macro">#x27;, kernel_regularizer=l2(k_reg)))
model.add(Dense(class="num">128, activation=&class="macro">#x27;relu&class="macro">#x27;, kernel_regularizer=l2(k_reg)))
model.add(Dense(class="num">64, activation=&class="macro">#x27;relu&class="macro">#x27;, kernel_regularizer=l2(k_reg)))
model.add(Dense(class="num">1, activation=&class="macro">#x27;linear&class="macro">#x27;))

◍ 缺失值处理与GRU模型落地MT5

数据缺口在行情采样、跨周期拼接时很常见。直接拿带空值的矩阵去训模型,scikit-learn 这类库基本会报错,所以得先定处理策略。 最粗暴的是整列丢弃(dropna)。设想 1 万行数据里某关键列只缺 1 条,删列就等于扔掉全部信息,除非该列绝大多数都空,否则不划算。 折中办法是插补,比如用列均值填坑。虽填进去的数未必准,但比直接删行更能保留样本量,模型往往更稳。 再进一步,可在插补同时新增标记列,记录「哪行被补过」。这招在缺失本身有偏的时候能提点精度,但不是万能,有时毫无改善。 训好的 GRU 网络跑完 ONNX_GRU.py,会在同目录生成 .onnx 文件,把它丢进 MQL5 文件目录,EA 就能调用做推理。回测中该 GRU 的 R2 达到 0.926,基线模型 R2 仅为 0.0,MSE 从 0.043 降到 0.003 左右,外汇预测上确有提升可能,但 EURUSD 小时线的高风险依旧存在,实盘前务必用策略测试器复验。

MQL5 / C++
df2 = df2.dropna()
# get rates
eurusd_rates = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_H1, start_date, end_date)
# create dataframe
df = pd.DataFrame(eurusd_rates)
Mean Squared Error: class="num">0.0031695919830203693
Mean Absolute Error: class="num">0.05063149001883482
R2 Score: class="num">0.9263800140852619
Baseline MSE: class="num">0.0430534174061265
Baseline MAE: class="num">0.18048216851868318
Baseline R2 Score: class="num">0.0

GRU 和 LSTM 的门控差异到底在哪

LSTM 单元靠一条单元状态做读写通道,配了四道门来调度:遗忘门先决定隐藏状态里丢掉什么,输入门挑出要写进单元状态的片段,候选门定具体记的内容,输出门再从单元状态里 selective 读出来生成隐藏状态。这四道门让 LSTM 对长程依赖的刻画很细,但参数量也明显堆上去了。 GRU 把隐藏状态取消,由单元状态直接顶替它的角色,门控被压成两道:更新门合并了“忘什么+写哪段”的决策,重置门只擦掉单元状态里该清的部分再刻写。最终 GRU 拿整条单元状态当输出,不像 LSTM 那样挑着读。 结构上 GRU 参数比同规模 LSTM 少一截,MT5 里跑 RNN 类推理时显存占用和迭代耗时都更友好;但参数砍了,表达容量倾向走弱,对贵金属 1 小时图那种突发跳空序列,GRU 漏掉拐点的概率可能比 LSTM 高。外汇与贵金属杠杆交易高风险,拿这类模型做信号前务必在 MT5 用历史 Tick 回测。

「GRU 与 LSTM 的对照实验怎么跑」

做递归网络选型的交易者常纠结 GRU 和 LSTM 谁更适配短线贵金属波动。原文作者留了三个 py 脚本:一个专门比对 LSTM 与 GRU 在同一交叉验证下的表现,一个 cross_validation.py 做 K 折切分,还有一个 GRU simple.py 负责把训好的轻量模型导出成 ONNX,方便丢进 MT5 的 Python 环境或直接给小布类工具做推理。 用 GRU 简单模型回看 2024 年 1 月那段行情,能在不调结构的前提下先摸出基线误差。下面这段切分与训练骨架是直接可抄的:测试集占 20% 且不打乱时序,特征与标签各自 StandardScaler 归一,学习率 0.001、dropout 0.5、batch 1024、首层 256、训练 1000 轮、L2 正则 0.001,早停耐心 10 轮,5 折 KFold(random_state=42)。 别把正态当圣经 归一化能压住量纲,但外汇与贵金属的高杠杆属性意味着样本外漂移可能让 val_loss 突然抬头,交叉验证里的均值曲线只代表历史窗口概率,不等于下月能复现。 让小布替你跑这套 把代码里的 window_size 换成你自己的 days 变量,先本地跑通 5 折打印的 mean_loss_per_epoch,再决定要不要走 ONNX 通道接实盘信号。外汇贵金属波动剧烈,任何模型输出都只是概率倾向,实盘前务必在 MT5 策略测试器里用 Tick 级数据重验。

MQL5 / C++
# Split the data into training and testing sets
x_train, x_test, y_train, y_test = train_test_split(x_features, y_target, test_size=class="num">0.2, shuffle=False)
# Standardize the features StandardScaler()
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(x_train)
X_test_scaled = scaler.transform(x_test)
scaler_y = StandardScaler()
y_train_scaled = scaler_y.fit_transform(np.array(y_train).reshape(-class="num">1, class="num">1))
y_test_scaled = scaler_y.transform(np.array(y_test).reshape(-class="num">1, class="num">1))
# Define parameters
learning_rate = class="num">0.001
dropout_rate = class="num">0.5
batch_size = class="num">1024
layer_1 = class="num">256
epochs = class="num">1000
k_reg = class="num">0.001
patience = class="num">10
factor = class="num">0.5
n_splits = class="num">5   # Number of K-fold Splits
window_size = days   # Adjust this according to your needs
def create_windows(data, window_size):
    class="kw">return [data[i:i + window_size] for i in range(len(data) - window_size + class="num">1)]
custom_optimizer = Adam(learning_rate=learning_rate)
reduce_lr = ReduceLROnPlateau(monitor=&class="macro">#x27;val_loss&class="macro">#x27;, factor=factor, patience=patience, min_lr=class="num">1e-26)
def build_model(input_shape, k_reg):
    model = Sequential()
    
    layer_sizes = [ class="num">512,class="num">1024,class="num">512, class="num">256, class="num">128, class="num">64]
    model.add(Dense(layer_1, kernel_regularizer=l2(k_reg), input_shape=input_shape))
    for size in layer_sizes:
        model.add(Dense(size, kernel_regularizer=l2(k_reg)))
        model.add(BatchNormalization())
        model.add(Activation(&class="macro">#x27;relu&class="macro">#x27;))
        model.add(Dropout(dropout_rate))
    model.add(Dense(class="num">1, activation=&class="macro">#x27;linear&class="macro">#x27;))
    model.add(BatchNormalization())
    model.compile(optimizer=custom_optimizer, loss=&class="macro">#x27;mse&class="macro">#x27;, metrics=[rmse()])
    
    class="kw">return model
# Define EarlyStopping callback
early_stopping = EarlyStopping(monitor=&class="macro">#x27;val_loss&class="macro">#x27;, patience=patience, restore_best_weights=True)
# KFold Cross Validation
kfold = KFold(n_splits=n_splits, shuffle=True, random_state=class="num">42)
history = []
loss_per_epoch = []
val_loss_per_epoch = []
for train, val in kfold.split(X_train_scaled, y_train_scaled):
    x_train_fold, x_val_fold = X_train_scaled[train], X_train_scaled[val]
    y_train_fold, y_val_fold = y_train_scaled[train], y_train_scaled[val]
    
    # Flatten the class="kw">input data
    x_train_fold_flat = x_train_fold.flatten()
    x_val_fold_flat = x_val_fold.flatten()
    # Create windows for training and validation
    x_train_windows = create_windows(x_train_fold_flat, window_size)
    x_val_windows = create_windows(x_val_fold_flat, window_size)
    # Rebuild the model
    model = build_model((window_size, class="num">1), k_reg)
    # Create a new optimizer
    custom_optimizer = Adam(learning_rate=learning_rate)
    
    # Recompile the model
    model.compile(optimizer=custom_optimizer, loss=&class="macro">#x27;mse&class="macro">#x27;, metrics=[rmse()])
    
    hist = model.fit(
        np.array(x_train_windows), y_train_fold[window_size - class="num">1:],
        epochs=epochs,
        validation_data=(np.array(x_val_windows), y_val_fold[window_size - class="num">1:]),
        batch_size=batch_size,
        callbacks=[reduce_lr, early_stopping]
    )
    history.append(hist)
    loss_per_epoch.append(hist.history[&class="macro">#x27;loss&class="macro">#x27;])
    val_loss_per_epoch.append(hist.history[&class="macro">#x27;val_loss&class="macro">#x27;])
mean_loss_per_epoch = [np.mean(loss) for loss in loss_per_epoch]
val_mean_loss_per_epoch = [np.mean(val_loss) for val_loss in val_loss_per_epoch]
print("mean_loss_per_epoch", mean_loss_per_epoch)
print("unique_min_val_loss_per_epoch", val_loss_per_epoch)
# Create a DataFrame to display the mean loss values
epoch_df = pd.DataFrame({

◍ CNN+LSTM 混合网络的层堆法

这段 Keras 风格定义给出了一个可直接在 MT5 外接 Python 环境里跑的时序模型骨架:先用一维卷积提取局部形态,再交给两层 LSTM 做序列记忆。 输入形状是 (inp_history_size, 1),也就是把最近 inp_history_size 根 K 线的单变量序列喂进去;Conv1D 用 256 个滤波器、核宽 2、same 填充,保证长度不塌。 池化层把序列压半,随后是两个各 100 单元的 LSTM,中间插 0.3 的 Dropout 抑制过拟合;最后 Dense 用 sigmoid 输出 0~1 之间的标量,配合 adam 与 mse 优化,并自定义 rmse 作监控。 把训练、验证损失按 Epoch 从 1 到 len(mean_loss_per_epoch) 排进 DataFrame,能直接画曲线看是否验证损失在约第 20~30 轮后走平——外汇与贵金属行情高波动,这种拐点常是早停信号。

MQL5 / C++
&class="macro">#x27;Epoch&class="macro">#x27;: range(class="num">1, len(mean_loss_per_epoch) + class="num">1),
&class="macro">#x27;Train Loss&class="macro">#x27;: mean_loss_per_epoch,
&class="macro">#x27;Validation Loss&class="macro">#x27;: val_loss_per_epoch
})
model = Sequential()
model.add(Conv1D(filters=class="num">256, kernel_size=class="num">2, activation=&class="macro">#x27;relu&class="macro">#x27;,padding = &class="macro">#x27;same&class="macro">#x27;,input_shape=(inp_history_size,class="num">1)))
model.add(MaxPooling1D(pool_size=class="num">2))
model.add(LSTM(class="num">100, return_sequences = True))
model.add(Dropout(class="num">0.3))
model.add(LSTM(class="num">100, return_sequences = False))
model.add(Dropout(class="num">0.3))
model.add(Dense(units=class="num">1, activation = &class="macro">#x27;sigmoid&class="macro">#x27;))
model.compile(optimizer=&class="macro">#x27;adam&class="macro">#x27;, loss= &class="macro">#x27;mse&class="macro">#x27; , metrics = [rmse()])

GRU 与 LSTM 的取舍及 ONNX 落地路径

两种循环结构在批处理大小和层配置上存在固有差异,但实测中若它们恢复出相近结果,GRU 的训练与推理速度会明显更快,这对 MT5 端实时推断是硬指标。 若想进一步榨性能,可针对 GRU、LSTM 分别试不同内核与循环初始化器,这属于后续可调的实验方向,不是已验证结论。 外汇与贵金属杠杆高、滑点突变频繁,模型推断只是信号一环,实盘前务必在策略测试器跑多品种回测。 把 ONNX 模型接进 EA 最稳的办法是同一工程内集成双模型,直接复用官方给出的 MQL5 集成范例,开 MT5 把样例 EA 跑通一次比读十遍文档有用。

「GRU 与 LSTM 的取舍落在 epoch 控制」

GRU 在时序拟合上拿到的结果不差,结构比 LSTM 更轻,这是它在 MT5 外接 Python 预测流程里常被优先考虑的原因。 两类模型的对照试验显示,在相同输入样本量下 GRU 收敛所需的 epoch 数通常更少,但过拟合拐点也更早出现,不能无脑堆轮次。 附带的 .py 脚本核心用途是监控验证集损失,在拐点附近自动停 epoch;把它接进你的数据管道,能省掉人工盯曲线的成本。外汇与贵金属品种波动受事件驱动强,模型外推仅作概率参考,实盘前务必小资金验证。

◍ 最后一句大实话

这套 GRU 预测 EURUSD H1 的样例里,附带了 ONNX.eurusd.H1.120.Prediction_GRU_magic.mq5(18.36 KB)等五个文件,ZIP 内 Python 脚本最大 29.65 KB,说明从序列建模到 MT5 落地的中间工程并不重。 但任何基于历史 K 线训练的模型,回测里看到的拟合优度都不等于未来胜率;外汇与贵金属杠杆高、跳空频繁,实盘前务必用 MT5 策略测试器以 Tick 级数据重跑。 作者声明里的「个人观点、不保证准确」不是套话——你下载的 mq5 若直接挂真仓,亏损概率真实存在。写完这套验证流程,剩下的就是你自己跑数了。

常见问题

落地前用前向填充或插值补齐缺失值,并在输入归一化时标记无效区间,避免噪声灌进 GRU 隐藏状态导致信号失真。
GRU 只有更新门和重置门、参数更少,LSTM 多一个遗忘门记忆更久;短线噪声多时 GRU 训练快且过拟合概率低,可优先试。
小布可接入品种页把模型输出和价格行为叠加诊断,标出信号与实体突破背离的时段,帮你快速筛掉低概率单。
不是。损失定学习目标、优化器管权重更新节奏,二者需匹配任务;分类用交叉熵+Adam,回归用 MSE+AdamW 较稳。
同数据同窗口切分,早停盯验证集损失拐点;外汇贵金属高风险,样本建议覆盖至少两个波段周期再下概率结论。