mql5/Experts/Advisors/DualEA/ML/model.py

364 lines
13 KiB
Python
Raw Permalink Normal View History

2025-08-10 16:25:37 -04:00
import os
from typing import Optional
import tensorflow as tf
def build_classifier(input_dim: int,
hidden: Optional[list[int]] = None,
2026-02-05 23:31:20 -05:00
dropout: float = 0.1,
use_batch_norm: bool = True,
use_residual: bool = True) -> tf.keras.Model:
"""Build an optimized binary classifier with BatchNorm and Residual connections.
Neurobook Chapter 6 optimizations:
- Batch Normalization: accelerates convergence, allows higher learning rates
- Residual connections: enables deeper networks without vanishing gradients
2025-08-10 16:25:37 -04:00
Args:
input_dim: number of input features
2026-02-05 23:31:20 -05:00
hidden: list of hidden layer sizes (default: [128, 64, 32])
2025-08-10 16:25:37 -04:00
dropout: dropout rate between dense layers
2026-02-05 23:31:20 -05:00
use_batch_norm: enable Batch Normalization (recommended)
use_residual: enable residual connections (recommended for deep nets)
2025-08-10 16:25:37 -04:00
"""
if hidden is None:
2026-02-05 23:31:20 -05:00
hidden = [128, 64, 32]
2025-08-10 16:25:37 -04:00
inputs = tf.keras.Input(shape=(input_dim,), name="features")
x = inputs
2026-02-05 23:31:20 -05:00
# Initial projection if needed for residual
prev_units = input_dim
2025-08-10 16:25:37 -04:00
for i, h in enumerate(hidden):
2026-02-05 23:31:20 -05:00
# Store input for residual connection
residual = x
# Dense layer with He initialization (Neurobook Chapter 1)
x = tf.keras.layers.Dense(
h,
activation=None, # Linear before BN
kernel_initializer='he_normal',
name=f"dense_{i}"
)(x)
# Batch Normalization (Neurobook Chapter 6)
if use_batch_norm:
x = tf.keras.layers.BatchNormalization(name=f"bn_{i}")(x)
# Activation after BN (best practice)
x = tf.keras.layers.Activation('relu', name=f"relu_{i}")(x)
# Dropout for regularization
2025-08-10 16:25:37 -04:00
if dropout and dropout > 0:
x = tf.keras.layers.Dropout(dropout, name=f"dropout_{i}")(x)
2026-02-05 23:31:20 -05:00
# Residual connection (Neurobook Chapter 6)
if use_residual and prev_units == h:
x = tf.keras.layers.Add(name=f"residual_{i}")([x, residual])
prev_units = h
outputs = tf.keras.layers.Dense(1, activation='sigmoid', name="p_win")(x)
model = tf.keras.Model(inputs=inputs, outputs=outputs, name="dualEA_optimized_classifier")
# Higher learning rate possible with BatchNorm (Neurobook Chapter 6)
model.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=3e-3), # 3x higher with BN
loss='binary_crossentropy',
metrics=[
tf.keras.metrics.AUC(name="auc"),
tf.keras.metrics.BinaryAccuracy(name="acc"),
tf.keras.metrics.Precision(name="precision"),
tf.keras.metrics.Recall(name="recall")
]
)
return model
def build_attention_lstm(input_dim: int,
seq_len: int = 30,
lstm_units: Optional[list[int]] = None,
attention_heads: int = 4,
dense: Optional[list[int]] = None,
dropout: float = 0.1,
use_bidirectional: bool = True) -> tf.keras.Model:
"""Build Attention-based LSTM with Bidirectional processing.
Neurobook Chapter 5 (Attention) + Chapter 4 (RNN) optimizations:
- Self-Attention: captures long-range dependencies better than LSTM alone
- Bidirectional: captures past AND future context
- 2-3x better pattern recognition for time series
Args:
input_dim: number of per-timestep features
seq_len: sequence length (timesteps)
lstm_units: list of LSTM hidden sizes (default: [128, 64])
attention_heads: number of attention heads (default: 4)
dense: list of dense layer sizes after LSTM
dropout: dropout rate applied after LSTM and dense layers
use_bidirectional: enable bidirectional processing (recommended)
"""
if lstm_units is None:
lstm_units = [128, 64]
if dense is None:
dense = [32]
inputs = tf.keras.Input(shape=(seq_len, input_dim), name="seq_features")
x = inputs
# Bidirectional LSTM layers (Neurobook Chapter 4)
for i, u in enumerate(lstm_units):
return_seq = True # Keep sequences for attention
lstm_layer = tf.keras.layers.LSTM(
u,
return_sequences=return_seq,
kernel_initializer='he_normal',
name=f"lstm_{i}"
)
if use_bidirectional and i == 0: # Bidirectional on first layer
x = tf.keras.layers.Bidirectional(
lstm_layer,
name=f"bilstm_{i}"
)(x)
else:
x = lstm_layer(x)
if dropout and dropout > 0:
x = tf.keras.layers.Dropout(dropout, name=f"lstm_dropout_{i}")(x)
# Multi-Head Self-Attention (Neurobook Chapter 5)
# Captures which time steps are most relevant
attention_output = tf.keras.layers.MultiHeadAttention(
num_heads=attention_heads,
key_dim=lstm_units[-1] // attention_heads,
name="self_attention"
)(x, x)
# Global average pooling across time dimension
x = tf.keras.layers.GlobalAveragePooling1D(name="temporal_pooling")(attention_output)
# Dense layers with Batch Normalization
for j, h in enumerate(dense):
x = tf.keras.layers.Dense(h, activation=None, name=f"post_dense_{j}")(x)
x = tf.keras.layers.BatchNormalization(name=f"post_bn_{j}")(x)
x = tf.keras.layers.Activation('relu', name=f"post_relu_{j}")(x)
if dropout and dropout > 0:
x = tf.keras.layers.Dropout(dropout, name=f"post_dropout_{j}")(x)
outputs = tf.keras.layers.Dense(1, activation='sigmoid', name="p_win")(x)
model = tf.keras.Model(inputs=inputs, outputs=outputs, name="dualEA_attention_lstm")
model.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),
loss='binary_crossentropy',
metrics=[
tf.keras.metrics.AUC(name="auc"),
tf.keras.metrics.BinaryAccuracy(name="acc"),
tf.keras.metrics.Precision(name="precision"),
tf.keras.metrics.Recall(name="recall")
]
)
return model
def build_gru(input_dim: int,
seq_len: int = 30,
gru_units: Optional[list[int]] = None,
dense: Optional[list[int]] = None,
dropout: float = 0.1) -> tf.keras.Model:
"""Build GRU-based model - 25-30% faster than LSTM with similar accuracy.
Neurobook Chapter 4: GRU is a lighter alternative to LSTM
- Fewer parameters (no cell state)
- Faster training and inference
- Similar performance on most time series tasks
Args:
input_dim: number of per-timestep features
seq_len: sequence length (timesteps)
gru_units: list of GRU hidden sizes (default: [128, 64])
dense: list of dense layer sizes after GRU
dropout: dropout rate applied after GRU and dense layers
"""
if gru_units is None:
gru_units = [128, 64]
if dense is None:
dense = [32]
inputs = tf.keras.Input(shape=(seq_len, input_dim), name="seq_features")
x = inputs
for i, u in enumerate(gru_units):
return_seq = (i < len(gru_units) - 1)
x = tf.keras.layers.GRU(
u,
return_sequences=return_seq,
kernel_initializer='he_normal',
name=f"gru_{i}"
)(x)
if dropout and dropout > 0:
x = tf.keras.layers.Dropout(dropout, name=f"gru_dropout_{i}")(x)
# Dense layers with Batch Normalization
for j, h in enumerate(dense):
x = tf.keras.layers.Dense(h, activation=None, name=f"post_dense_{j}")(x)
x = tf.keras.layers.BatchNormalization(name=f"post_bn_{j}")(x)
x = tf.keras.layers.Activation('relu', name=f"post_relu_{j}")(x)
if dropout and dropout > 0:
x = tf.keras.layers.Dropout(dropout, name=f"post_dropout_{j}")(x)
outputs = tf.keras.layers.Dense(1, activation='sigmoid', name="p_win")(x)
model = tf.keras.Model(inputs=inputs, outputs=outputs, name="dualEA_gru")
model.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),
loss='binary_crossentropy',
metrics=[
tf.keras.metrics.AUC(name="auc"),
tf.keras.metrics.BinaryAccuracy(name="acc")
]
)
2025-08-10 16:25:37 -04:00
return model
def build_lstm(input_dim: int,
seq_len: int = 30,
lstm_units: Optional[list[int]] = None,
dense: Optional[list[int]] = None,
2026-02-05 23:31:20 -05:00
dropout: float = 0.1,
use_batch_norm: bool = True) -> tf.keras.Model:
"""Build an optimized LSTM-based binary classifier.
Legacy wrapper with Neurobook Chapter 6 improvements (BatchNorm).
2025-08-10 16:25:37 -04:00
Args:
input_dim: number of per-timestep features
seq_len: sequence length (timesteps)
2026-02-05 23:31:20 -05:00
lstm_units: list of LSTM hidden sizes (default: [128, 64])
2025-08-10 16:25:37 -04:00
dense: list of dense layer sizes after LSTM
dropout: dropout rate applied after LSTM and dense layers
2026-02-05 23:31:20 -05:00
use_batch_norm: enable Batch Normalization
2025-08-10 16:25:37 -04:00
"""
if lstm_units is None:
2026-02-05 23:31:20 -05:00
lstm_units = [128, 64]
2025-08-10 16:25:37 -04:00
if dense is None:
dense = [32]
inputs = tf.keras.Input(shape=(seq_len, input_dim), name="seq_features")
x = inputs
2026-02-05 23:31:20 -05:00
2025-08-10 16:25:37 -04:00
for i, u in enumerate(lstm_units):
return_seq = (i < len(lstm_units) - 1)
2026-02-05 23:31:20 -05:00
x = tf.keras.layers.LSTM(
u,
return_sequences=return_seq,
kernel_initializer='he_normal',
name=f"lstm_{i}"
)(x)
2025-08-10 16:25:37 -04:00
if dropout and dropout > 0:
x = tf.keras.layers.Dropout(dropout, name=f"lstm_dropout_{i}")(x)
2026-02-05 23:31:20 -05:00
2025-08-10 16:25:37 -04:00
for j, h in enumerate(dense):
2026-02-05 23:31:20 -05:00
x = tf.keras.layers.Dense(h, activation=None, name=f"post_dense_{j}")(x)
if use_batch_norm:
x = tf.keras.layers.BatchNormalization(name=f"post_bn_{j}")(x)
x = tf.keras.layers.Activation('relu', name=f"post_relu_{j}")(x)
2025-08-10 16:25:37 -04:00
if dropout and dropout > 0:
x = tf.keras.layers.Dropout(dropout, name=f"post_dropout_{j}")(x)
2026-02-05 23:31:20 -05:00
outputs = tf.keras.layers.Dense(1, activation='sigmoid', name="p_win")(x)
2025-08-10 16:25:37 -04:00
model = tf.keras.Model(inputs=inputs, outputs=outputs, name="dualEA_win_lstm")
2026-02-05 23:31:20 -05:00
model.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),
loss='binary_crossentropy',
metrics=[
tf.keras.metrics.AUC(name="auc"),
tf.keras.metrics.BinaryAccuracy(name="acc"),
tf.keras.metrics.Precision(name="precision"),
tf.keras.metrics.Recall(name="recall")
]
)
2025-08-10 16:25:37 -04:00
return model
2026-02-05 23:31:20 -05:00
def build_model(model_type: str = "classifier", **kwargs) -> tf.keras.Model:
"""Factory function to build any model type by name.
Available models (per Neurobook recommendations):
- "classifier": Fast feed-forward with BatchNorm + Residual (Chapter 6)
- "attention_lstm": Best accuracy with Self-Attention (Chapter 5)
- "gru": 25-30% faster than LSTM (Chapter 4)
- "lstm": Optimized LSTM with BatchNorm (Chapter 6)
Args:
model_type: one of ["classifier", "attention_lstm", "gru", "lstm"]
**kwargs: passed to the specific builder function
Returns:
Compiled tf.keras.Model
"""
builders = {
"classifier": build_classifier,
"attention_lstm": build_attention_lstm,
"gru": build_gru,
"lstm": build_lstm,
}
if model_type not in builders:
raise ValueError(f"Unknown model_type: {model_type}. Choose from {list(builders.keys())}")
return builders[model_type](**kwargs)
def export_to_onnx(model: tf.keras.Model, out_dir: str, opset: int = 13) -> str:
"""Export Keras model to ONNX format for MQL5 integration.
Neurobook Chapter 3: ONNX Runtime integration for MQL5
- Use opset 13+ for best compatibility
- Dynamic batch size for inference flexibility
Args:
model: trained tf.keras.Model
out_dir: output directory
opset: ONNX opset version (default 13)
Returns:
Path to exported .onnx file
"""
import tf2onnx
import onnx
os.makedirs(out_dir, exist_ok=True)
onnx_path = os.path.join(out_dir, "model.onnx")
# Convert to ONNX
spec = (tf.TensorSpec((None,) + model.input_shape[1:], tf.float32, name="input"),)
model_proto, _ = tf2onnx.convert.from_keras(model, input_signature=spec, opset=opset)
# Save
onnx.save(model_proto, onnx_path)
return onnx_path
2025-08-10 16:25:37 -04:00
def save_model(model: tf.keras.Model, out_dir: str) -> str:
os.makedirs(out_dir, exist_ok=True)
path = os.path.join(out_dir, "tf_model.keras")
model.save(path)
return path
def load_model(model_dir: str) -> tf.keras.Model:
path = os.path.join(model_dir, "tf_model.keras")
return tf.keras.models.load_model(path)