839 lines
28 KiB
Plaintext
839 lines
28 KiB
Plaintext
#ifndef NEURAL_NET_MQH
|
||
#define NEURAL_NET_MQH
|
||
#include "Statistics.mqh"
|
||
|
||
class CNeuralNet {
|
||
private:
|
||
int m_inputs;
|
||
int m_hidden;
|
||
int m_outputs;
|
||
|
||
matrix m_W1;
|
||
vector m_b1;
|
||
matrix m_W2;
|
||
vector m_b2;
|
||
|
||
matrix m_mW1, m_vW1;
|
||
vector m_mb1, m_vb1;
|
||
matrix m_mW2, m_vW2;
|
||
vector m_mb2, m_vb2;
|
||
int m_t;
|
||
double m_adamBeta1;
|
||
double m_adamBeta2;
|
||
double m_adamEps;
|
||
double m_beta1T;
|
||
double m_beta2T;
|
||
|
||
double m_l2;
|
||
int m_epochsTrained;
|
||
double m_lastLoss;
|
||
bool m_initialized;
|
||
|
||
// HeInit per ReLU (NeuroBook §1.3)
|
||
double HeScale(int fanIn) const {
|
||
return MathSqrt(6.0 / MathMax(1, fanIn));
|
||
}
|
||
|
||
// Dropout (NeuroBook §6.2)
|
||
double m_dropoutRate;
|
||
double m_dropoutMask[];
|
||
|
||
// BatchNorm semplificata su hidden (NeuroBook §6.3)
|
||
// Running stats (EMA) + learnable affine
|
||
vector m_bnGamma; // scala learnable
|
||
vector m_bnBeta; // shift learnable
|
||
vector m_bnRunningMean; // media mobile
|
||
vector m_bnRunningVar; // varianza mobile
|
||
double m_bnMomentum;
|
||
double m_bnEps;
|
||
|
||
// Debug: loss history
|
||
double m_lossHistory[];
|
||
string m_lossCsvFn;
|
||
|
||
void ApplyDropout(vector &h) {
|
||
for(int i = 0; i < h.Size(); i++) {
|
||
m_dropoutMask[i] = ((double)MathRand() / 32767.0) > m_dropoutRate ? 1.0 : 0.0;
|
||
h[i] *= m_dropoutMask[i];
|
||
}
|
||
}
|
||
|
||
void ApplyBN(vector &h) {
|
||
// Normalizza h IN PLACE: per-neuron stats (NeuroBook §6.3)
|
||
for(int i = 0; i < h.Size(); i++) {
|
||
double denom = MathSqrt(m_bnRunningVar[i] + m_bnEps);
|
||
double standardized = (h[i] - m_bnRunningMean[i]) / denom;
|
||
h[i] = m_bnGamma[i] * standardized + m_bnBeta[i];
|
||
}
|
||
}
|
||
|
||
// Normalizza E AGGIORNA running stats (training, batch_size=1).
|
||
// Usa running stats per normalizzare (stessa modalità di inference) per
|
||
// evitare degenerazione con batch=1 (var=0). (NeuroBook §6.3, online BN)
|
||
void TrainBN(const vector &h_in, vector &h_out) {
|
||
h_out.Resize(h_in.Size());
|
||
for(int i = 0; i < h_in.Size(); i++) {
|
||
// Update running stats (per-neuron EMA)
|
||
m_bnRunningMean[i] = m_bnMomentum * m_bnRunningMean[i] + (1.0 - m_bnMomentum) * h_in[i];
|
||
double dx = h_in[i] - m_bnRunningMean[i];
|
||
m_bnRunningVar[i] = m_bnMomentum * m_bnRunningVar[i] + (1.0 - m_bnMomentum) * dx * dx;
|
||
// Normalize using running stats (same as ApplyBN)
|
||
double denom = MathSqrt(m_bnRunningVar[i] + m_bnEps);
|
||
double standardized = (h_in[i] - m_bnRunningMean[i]) / denom;
|
||
h_out[i] = m_bnGamma[i] * standardized + m_bnBeta[i];
|
||
}
|
||
}
|
||
|
||
// Backward attraverso BN: dh_out[i] = dh_norm[i] * gamma[i] / sqrt(runningVar[i] + eps)
|
||
void BNBackward(vector &dh_out, const vector &h_in, const vector &dh_norm) {
|
||
dh_out.Resize(h_in.Size());
|
||
for(int i = 0; i < h_in.Size(); i++) {
|
||
double denom = MathSqrt(m_bnRunningVar[i] + m_bnEps);
|
||
dh_out[i] = dh_norm[i] * m_bnGamma[i] / denom;
|
||
}
|
||
}
|
||
|
||
double ReLU(double x) const {
|
||
return (x > 0.0) ? x : 0.0;
|
||
}
|
||
|
||
double ReLUDeriv(double x) const {
|
||
return (x > 0.0) ? 1.0 : 0.0;
|
||
}
|
||
|
||
void Softmax(vector &v) const {
|
||
double maxVal = v[0];
|
||
for(int i = 1; i < v.Size(); i++)
|
||
if(v[i] > maxVal) maxVal = v[i];
|
||
|
||
double sum = 0.0;
|
||
for(int i = 0; i < v.Size(); i++) {
|
||
v[i] = MathExp(v[i] - maxVal);
|
||
sum += v[i];
|
||
}
|
||
double eps = DATA_EPS(sum);
|
||
if(sum > eps) {
|
||
for(int i = 0; i < v.Size(); i++)
|
||
v[i] /= sum;
|
||
} else {
|
||
double eq = 1.0 / v.Size();
|
||
for(int i = 0; i < v.Size(); i++)
|
||
v[i] = eq;
|
||
}
|
||
}
|
||
|
||
void BiasInit(vector &v) {
|
||
for(int i = 0; i < v.Size(); i++)
|
||
v[i] = 0.0;
|
||
}
|
||
|
||
void ZeroMatrix(matrix &m) {
|
||
for(int r = 0; r < (int)m.Rows(); r++)
|
||
for(int c = 0; c < (int)m.Cols(); c++)
|
||
m[r][c] = 0.0;
|
||
}
|
||
|
||
void ZeroVector(vector &v) {
|
||
for(int i = 0; i < v.Size(); i++)
|
||
v[i] = 0.0;
|
||
}
|
||
|
||
void AdamUpdate(matrix ¶m, matrix &m, matrix &v, matrix &grad, double lr) {
|
||
for(int r = 0; r < (int)param.Rows(); r++) {
|
||
for(int c = 0; c < (int)param.Cols(); c++) {
|
||
double g = grad[r][c] + m_l2 * param[r][c];
|
||
|
||
m[r][c] = m_adamBeta1 * m[r][c] + (1.0 - m_adamBeta1) * g;
|
||
v[r][c] = m_adamBeta2 * v[r][c] + (1.0 - m_adamBeta2) * g * g;
|
||
|
||
double mHat = m[r][c] / (1.0 - m_beta1T);
|
||
double vHat = v[r][c] / (1.0 - m_beta2T);
|
||
|
||
param[r][c] -= lr * mHat / (MathSqrt(vHat) + m_adamEps);
|
||
}
|
||
}
|
||
}
|
||
|
||
void AdamUpdate(vector ¶m, vector &m, vector &v, vector &grad, double lr) {
|
||
for(int i = 0; i < param.Size(); i++) {
|
||
double g = grad[i] + m_l2 * param[i];
|
||
|
||
m[i] = m_adamBeta1 * m[i] + (1.0 - m_adamBeta1) * g;
|
||
v[i] = m_adamBeta2 * v[i] + (1.0 - m_adamBeta2) * g * g;
|
||
|
||
double mHat = m[i] / (1.0 - m_beta1T);
|
||
double vHat = v[i] / (1.0 - m_beta2T);
|
||
|
||
param[i] -= lr * mHat / (MathSqrt(vHat) + m_adamEps);
|
||
}
|
||
}
|
||
|
||
public:
|
||
CNeuralNet()
|
||
: m_inputs(0), m_hidden(0), m_outputs(0),
|
||
m_t(0), m_beta1T(1.0), m_beta2T(1.0),
|
||
m_l2(0), m_epochsTrained(0), m_lastLoss(0.0),
|
||
m_initialized(false),
|
||
m_dropoutRate(0), m_bnMomentum(0), m_bnEps(0) {}
|
||
|
||
void Init(int inputs, int hidden, int outputs, double l2 = -1, double dropoutRate = -1) {
|
||
m_inputs = inputs;
|
||
m_hidden = hidden;
|
||
m_outputs = outputs;
|
||
|
||
// Iperparametri dall'architettura:
|
||
// hidden min. = 2 (un neurone non può fare computazione utile)
|
||
// β₂/β₁ ratio = 100 (Adam originale: 0.999/0.9)
|
||
// BN window = 5× hidden (momentum più lento per layer più grandi)
|
||
int totalParams = inputs * hidden + hidden + hidden * outputs + outputs;
|
||
int minHidden = MathMax(hidden, 2);
|
||
m_l2 = 1.0 / MathMax(totalParams, 1); // L2 = 1/param (1 = nessun parametro → L2=1)
|
||
m_dropoutRate = 1.0 / MathSqrt((double)minHidden); // Dropout: 1/sqrt(hidden)
|
||
m_adamBeta1 = 1.0 - 1.0 / (double)minHidden; // β₁ = 1 - 1/hidden
|
||
m_adamBeta2 = 1.0 - 1.0 / (double)MathMax(minHidden * 100, 200); // β₂ = 1 - 1/(hidden×100)
|
||
m_adamEps = DATA_EPS(1.0); // Adam ε: machine epsilon
|
||
m_bnMomentum = 1.0 - 1.0 / (double)MathMax(minHidden * 5, 10); // BN momentum = 1 - 1/(hidden×5)
|
||
m_bnEps = DATA_EPS(1.0); // BN ε: machine epsilon
|
||
|
||
// Se chiamata esterna vuole override, usa quelli
|
||
if(l2 >= 0) m_l2 = l2;
|
||
if(dropoutRate >= 0) m_dropoutRate = dropoutRate;
|
||
|
||
m_W1.Init(inputs, hidden);
|
||
m_b1.Init(hidden);
|
||
m_W2.Init(hidden, outputs);
|
||
m_b2.Init(outputs);
|
||
|
||
// He Init per ReLU (NeuroBook §1.3)
|
||
{
|
||
double scale = HeScale(inputs);
|
||
MathSrand(GetTickCount());
|
||
for(int r = 0; r < inputs; r++)
|
||
for(int c = 0; c < hidden; c++)
|
||
m_W1[r][c] = ((double)MathRand() / 32767.0 * 2.0 - 1.0) * scale;
|
||
}
|
||
BiasInit(m_b1);
|
||
{
|
||
double scale = HeScale(hidden);
|
||
for(int r = 0; r < hidden; r++)
|
||
for(int c = 0; c < outputs; c++)
|
||
m_W2[r][c] = ((double)MathRand() / 32767.0 * 2.0 - 1.0) * scale;
|
||
}
|
||
BiasInit(m_b2);
|
||
|
||
m_mW1.Init(inputs, hidden); ZeroMatrix(m_mW1);
|
||
m_vW1.Init(inputs, hidden); ZeroMatrix(m_vW1);
|
||
m_mb1.Init(hidden); ZeroVector(m_mb1);
|
||
m_vb1.Init(hidden); ZeroVector(m_vb1);
|
||
m_mW2.Init(hidden, outputs); ZeroMatrix(m_mW2);
|
||
m_vW2.Init(hidden, outputs); ZeroMatrix(m_vW2);
|
||
m_mb2.Init(outputs); ZeroVector(m_mb2);
|
||
m_vb2.Init(outputs); ZeroVector(m_vb2);
|
||
|
||
// Dropout mask
|
||
ArrayResize(m_dropoutMask, hidden);
|
||
|
||
// BatchNorm (NeuroBook §6.3)
|
||
m_bnGamma.Init(hidden);
|
||
m_bnBeta.Init(hidden);
|
||
m_bnRunningMean.Init(hidden);
|
||
m_bnRunningVar.Init(hidden);
|
||
for(int i = 0; i < hidden; i++) {
|
||
m_bnGamma[i] = 1.0;
|
||
m_bnBeta[i] = 0.0;
|
||
m_bnRunningMean[i] = 0.0;
|
||
m_bnRunningVar[i] = 1.0;
|
||
}
|
||
|
||
m_t = 0;
|
||
m_beta1T = 1.0;
|
||
m_beta2T = 1.0;
|
||
m_epochsTrained = 0;
|
||
m_lastLoss = 0.0;
|
||
m_initialized = true;
|
||
ArrayResize(m_lossHistory, 0);
|
||
m_lossCsvFn = "";
|
||
}
|
||
|
||
void ForwardPass(vector &inp, vector &output, vector &h1Cache) {
|
||
h1Cache.Resize(m_hidden);
|
||
for(int i = 0; i < m_hidden; i++) {
|
||
double sum = m_b1[i];
|
||
for(int j = 0; j < m_inputs; j++)
|
||
sum += inp[j] * m_W1[j][i];
|
||
h1Cache[i] = ReLU(sum);
|
||
}
|
||
// BatchNorm inferenza: normalizza con running stats (NeuroBook §6.3)
|
||
ApplyBN(h1Cache);
|
||
// Dropout scaling in inferenza: scale = 1 - dropoutRate (NeuroBook §6.2)
|
||
if(m_dropoutRate > 0) {
|
||
for(int i = 0; i < h1Cache.Size(); i++)
|
||
h1Cache[i] *= (1.0 - m_dropoutRate);
|
||
}
|
||
|
||
output.Resize(m_outputs);
|
||
for(int i = 0; i < m_outputs; i++) {
|
||
double sum = m_b2[i];
|
||
for(int j = 0; j < m_hidden; j++)
|
||
sum += h1Cache[j] * m_W2[j][i];
|
||
output[i] = sum;
|
||
}
|
||
Softmax(output);
|
||
}
|
||
|
||
void Forward(vector &inp, vector &output) {
|
||
vector h1Cache;
|
||
ForwardPass(inp, output, h1Cache);
|
||
}
|
||
|
||
double TrainSample(vector &inp, vector &target, double lr, double weight = 1.0) {
|
||
if(!m_initialized) return -1.0;
|
||
|
||
// ── Forward ──
|
||
// Layer 1: W1*x + b1 → z1 → ReLU → h1_raw → Dropout → h1_drop → BN → h1_norm
|
||
vector z1(m_hidden);
|
||
vector h1_raw(m_hidden);
|
||
for(int i = 0; i < m_hidden; i++) {
|
||
z1[i] = m_b1[i];
|
||
for(int j = 0; j < m_inputs; j++)
|
||
z1[i] += inp[j] * m_W1[j][i];
|
||
h1_raw[i] = ReLU(z1[i]);
|
||
}
|
||
|
||
// Dropout (NeuroBook §6.2): salva in m_dropoutMask, applica a h1_drop
|
||
vector h1_drop(m_hidden);
|
||
if(m_dropoutRate > 0.0) {
|
||
for(int i = 0; i < m_hidden; i++) {
|
||
m_dropoutMask[i] = ((double)MathRand() / 32767.0) > m_dropoutRate ? 1.0 : 0.0;
|
||
h1_drop[i] = h1_raw[i] * m_dropoutMask[i];
|
||
}
|
||
} else {
|
||
for(int i = 0; i < m_hidden; i++)
|
||
h1_drop[i] = h1_raw[i];
|
||
}
|
||
|
||
// BatchNorm (NeuroBook §6.3): normalizza h1_drop → h1_norm, aggiorna running stats
|
||
vector h1_cache = h1_drop; // copia per backward
|
||
vector h1_norm(m_hidden);
|
||
TrainBN(h1_drop, h1_norm);
|
||
|
||
// Layer 2: W2*h1_norm + b2 → z2 → Softmax → output
|
||
vector z2(m_outputs);
|
||
for(int i = 0; i < m_outputs; i++) {
|
||
z2[i] = m_b2[i];
|
||
for(int j = 0; j < m_hidden; j++)
|
||
z2[i] += h1_norm[j] * m_W2[j][i];
|
||
}
|
||
vector output(m_outputs);
|
||
for(int i = 0; i < m_outputs; i++) output[i] = z2[i];
|
||
Softmax(output);
|
||
|
||
// ── Loss: CCE pesata + L2 ──
|
||
double loss = 0.0;
|
||
for(int i = 0; i < m_outputs; i++) {
|
||
double p = MathMax(DATA_EPS(output[i]), output[i]);
|
||
loss -= weight * target[i] * MathLog(p);
|
||
}
|
||
loss += 0.5 * m_l2 * (L2Norm(m_W1) + L2Norm(m_W2));
|
||
m_lastLoss = loss;
|
||
|
||
m_t++;
|
||
m_beta1T *= m_adamBeta1;
|
||
m_beta2T *= m_adamBeta2;
|
||
|
||
// ── Backward ──
|
||
// dL/dz2 = weight * (output - target) (CCE+Softmax combinata, pesata)
|
||
vector dL_dz2(m_outputs);
|
||
for(int i = 0; i < m_outputs; i++)
|
||
dL_dz2[i] = weight * (output[i] - target[i]);
|
||
|
||
// dL/dW2 = h1_norm ⊗ dL/dz2
|
||
matrix dL_dW2(m_hidden, m_outputs);
|
||
for(int i = 0; i < m_hidden; i++)
|
||
for(int j = 0; j < m_outputs; j++)
|
||
dL_dW2[i][j] = h1_norm[i] * dL_dz2[j];
|
||
|
||
// dL/db2 = dL/dz2
|
||
vector dL_db2(m_outputs);
|
||
for(int i = 0; i < m_outputs; i++)
|
||
dL_db2[i] = dL_dz2[i];
|
||
|
||
// dL/dh1_norm = W2^T * dL/dz2
|
||
vector dL_dh1_norm(m_hidden);
|
||
for(int i = 0; i < m_hidden; i++) {
|
||
double sum = 0.0;
|
||
for(int j = 0; j < m_outputs; j++)
|
||
sum += dL_dz2[j] * m_W2[i][j];
|
||
dL_dh1_norm[i] = sum;
|
||
}
|
||
|
||
// BatchNorm backward: dL/dh1_drop = gamma/sqrt(var) * dL/dh1_norm
|
||
// + update gamma, beta
|
||
vector dL_dh1_drop(m_hidden);
|
||
for(int i = 0; i < m_hidden; i++) {
|
||
double denom = MathSqrt(m_bnRunningVar[i] + m_bnEps);
|
||
double h_std = (h1_cache[i] - m_bnRunningMean[i]) / denom;
|
||
double gOld = m_bnGamma[i]; // salva gamma pre-update per backward pass-through
|
||
m_bnGamma[i] -= lr * dL_dh1_norm[i] * h_std;
|
||
m_bnBeta[i] -= lr * dL_dh1_norm[i];
|
||
dL_dh1_drop[i] = dL_dh1_norm[i] * gOld / denom;
|
||
}
|
||
|
||
// Dropout backward: applica stessa mask
|
||
if(m_dropoutRate > 0.0) {
|
||
for(int i = 0; i < m_hidden; i++)
|
||
dL_dh1_drop[i] *= m_dropoutMask[i];
|
||
}
|
||
|
||
// dL/dz1 = dL/dh1_drop * ReLU'(z1)
|
||
vector dL_dz1(m_hidden);
|
||
for(int i = 0; i < m_hidden; i++)
|
||
dL_dz1[i] = dL_dh1_drop[i] * ReLUDeriv(z1[i]);
|
||
|
||
// dL/dW1 = inp ⊗ dL/dz1
|
||
matrix dL_dW1(m_inputs, m_hidden);
|
||
for(int i = 0; i < m_inputs; i++)
|
||
for(int j = 0; j < m_hidden; j++)
|
||
dL_dW1[i][j] = inp[i] * dL_dz1[j];
|
||
|
||
// dL/db1 = dL/dz1
|
||
vector dL_db1(m_hidden);
|
||
for(int i = 0; i < m_hidden; i++)
|
||
dL_db1[i] = dL_dz1[i];
|
||
|
||
// Adam update
|
||
AdamUpdate(m_W1, m_mW1, m_vW1, dL_dW1, lr);
|
||
AdamUpdate(m_b1, m_mb1, m_vb1, dL_db1, lr);
|
||
AdamUpdate(m_W2, m_mW2, m_vW2, dL_dW2, lr);
|
||
AdamUpdate(m_b2, m_mb2, m_vb2, dL_db2, lr);
|
||
|
||
return loss;
|
||
}
|
||
|
||
double L2Norm(matrix &m) {
|
||
double sum = 0.0;
|
||
for(int r = 0; r < (int)m.Rows(); r++)
|
||
for(int c = 0; c < (int)m.Cols(); c++)
|
||
sum += m[r][c] * m[r][c];
|
||
return sum;
|
||
}
|
||
|
||
// Overload senza pesi (compatibilità)
|
||
double Train(matrix &features, matrix &targets, int epochs, double lr) {
|
||
vector empty;
|
||
return Train(features, targets, epochs, lr, empty);
|
||
}
|
||
|
||
double Train(matrix &features, matrix &targets, int epochs, double lr, vector &weights) {
|
||
if(!m_initialized || features.Rows() == 0) return -1.0;
|
||
|
||
int n = (int)features.Rows();
|
||
double avgLoss = 0.0;
|
||
ArrayResize(m_lossHistory, epochs);
|
||
int logEvery = MathMax(1, epochs / 10);
|
||
|
||
for(int epoch = 0; epoch < epochs; epoch++) {
|
||
int indices[];
|
||
ArrayResize(indices, n);
|
||
for(int i = 0; i < n; i++) indices[i] = i;
|
||
|
||
for(int i = n - 1; i > 0; i--) {
|
||
int j = MathRand() % (i + 1);
|
||
int tmp = indices[i];
|
||
indices[i] = indices[j];
|
||
indices[j] = tmp;
|
||
}
|
||
|
||
double epochLoss = 0.0;
|
||
for(int s = 0; s < n; s++) {
|
||
int idx = indices[s];
|
||
vector inp = features.Row(idx);
|
||
vector tgt = targets.Row(idx);
|
||
double w = (weights.Size() > idx) ? weights[idx] : 1.0;
|
||
epochLoss += TrainSample(inp, tgt, lr, w);
|
||
}
|
||
epochLoss /= (double)n;
|
||
m_lossHistory[epoch] = epochLoss;
|
||
|
||
if(epoch == epochs - 1) avgLoss = epochLoss;
|
||
|
||
if(epoch == 0 || epoch == epochs - 1 || (epoch+1) % logEvery == 0) {
|
||
Print(" Epoch ", epoch+1, "/", epochs,
|
||
" | loss: ", StringFormat("%.6f", epochLoss),
|
||
" | lr: ", StringFormat("%.5f", lr));
|
||
}
|
||
}
|
||
|
||
m_epochsTrained += epochs;
|
||
m_lastLoss = avgLoss;
|
||
return avgLoss;
|
||
}
|
||
|
||
// Salva loss history in CSV nella cartella Common
|
||
bool SaveLossCsv(string filename = "") {
|
||
if(ArraySize(m_lossHistory) == 0) return false;
|
||
if(filename == "") filename = "NN_LossHistory.csv";
|
||
int fh = FileOpen(filename, FILE_TXT|FILE_WRITE|FILE_COMMON);
|
||
if(fh == INVALID_HANDLE) return false;
|
||
|
||
FileWriteString(fh, "epoch,loss\r\n");
|
||
for(int i = 0; i < ArraySize(m_lossHistory); i++) {
|
||
FileWriteString(fh, (string)(i+1) + "," + StringFormat("%.8f", m_lossHistory[i]) + "\r\n");
|
||
}
|
||
FileClose(fh);
|
||
Print("Loss history saved to ", filename, " (", ArraySize(m_lossHistory), " epochs)");
|
||
return true;
|
||
}
|
||
|
||
string LossHistorySummary() const {
|
||
if(ArraySize(m_lossHistory) == 0) return "no history";
|
||
double first = m_lossHistory[0];
|
||
double last = m_lossHistory[ArraySize(m_lossHistory)-1];
|
||
double best = first;
|
||
int bestEpoch = 0;
|
||
for(int i = 0; i < ArraySize(m_lossHistory); i++) {
|
||
if(m_lossHistory[i] < best) { best = m_lossHistory[i]; bestEpoch = i; }
|
||
}
|
||
return StringFormat("loss: %.6f → %.6f (best: %.6f @ epoch %d, %d epochs)",
|
||
first, last, best, bestEpoch+1, ArraySize(m_lossHistory));
|
||
}
|
||
|
||
int Predict(vector &inp) {
|
||
vector output;
|
||
Forward(inp, output);
|
||
int bestIdx = 0;
|
||
double bestVal = output[0];
|
||
for(int i = 1; i < m_outputs; i++) {
|
||
if(output[i] > bestVal) {
|
||
bestVal = output[i];
|
||
bestIdx = i;
|
||
}
|
||
}
|
||
return bestIdx;
|
||
}
|
||
|
||
double GetCombinedZ(vector &inp) {
|
||
vector output;
|
||
Forward(inp, output);
|
||
return output[0] - output[2];
|
||
}
|
||
|
||
bool Save(string filename) {
|
||
int fh = FileOpen(filename, FILE_WRITE | FILE_BIN | FILE_COMMON);
|
||
if(fh == INVALID_HANDLE) return false;
|
||
bool ok = Save(fh);
|
||
FileClose(fh);
|
||
return ok;
|
||
}
|
||
|
||
bool Save(int fh) {
|
||
if(fh == INVALID_HANDLE) return false;
|
||
|
||
FileWriteInteger(fh, 3);
|
||
FileWriteInteger(fh, m_inputs);
|
||
FileWriteInteger(fh, m_hidden);
|
||
FileWriteInteger(fh, m_outputs);
|
||
FileWriteInteger(fh, m_epochsTrained);
|
||
FileWriteDouble(fh, m_lastLoss);
|
||
FileWriteDouble(fh, m_l2);
|
||
FileWriteDouble(fh, m_dropoutRate);
|
||
FileWriteDouble(fh, m_bnMomentum);
|
||
FileWriteDouble(fh, m_bnEps);
|
||
|
||
for(int r = 0; r < m_inputs; r++)
|
||
for(int c = 0; c < m_hidden; c++)
|
||
FileWriteDouble(fh, m_W1[r][c]);
|
||
|
||
for(int i = 0; i < m_hidden; i++)
|
||
FileWriteDouble(fh, m_b1[i]);
|
||
|
||
for(int r = 0; r < m_hidden; r++)
|
||
for(int c = 0; c < m_outputs; c++)
|
||
FileWriteDouble(fh, m_W2[r][c]);
|
||
|
||
for(int i = 0; i < m_outputs; i++)
|
||
FileWriteDouble(fh, m_b2[i]);
|
||
|
||
for(int r = 0; r < m_inputs; r++)
|
||
for(int c = 0; c < m_hidden; c++) {
|
||
FileWriteDouble(fh, m_mW1[r][c]);
|
||
FileWriteDouble(fh, m_vW1[r][c]);
|
||
}
|
||
|
||
for(int i = 0; i < m_hidden; i++) {
|
||
FileWriteDouble(fh, m_mb1[i]);
|
||
FileWriteDouble(fh, m_vb1[i]);
|
||
}
|
||
|
||
for(int r = 0; r < m_hidden; r++)
|
||
for(int c = 0; c < m_outputs; c++) {
|
||
FileWriteDouble(fh, m_mW2[r][c]);
|
||
FileWriteDouble(fh, m_vW2[r][c]);
|
||
}
|
||
|
||
for(int i = 0; i < m_outputs; i++) {
|
||
FileWriteDouble(fh, m_mb2[i]);
|
||
FileWriteDouble(fh, m_vb2[i]);
|
||
}
|
||
|
||
FileWriteDouble(fh, m_beta1T);
|
||
FileWriteDouble(fh, m_beta2T);
|
||
FileWriteInteger(fh, m_t);
|
||
|
||
// BatchNorm (v3)
|
||
for(int i = 0; i < m_hidden; i++) {
|
||
FileWriteDouble(fh, m_bnGamma[i]);
|
||
FileWriteDouble(fh, m_bnBeta[i]);
|
||
FileWriteDouble(fh, m_bnRunningMean[i]);
|
||
FileWriteDouble(fh, m_bnRunningVar[i]);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
bool Load(string filename) {
|
||
if(!FileIsExist(filename, FILE_COMMON)) return false;
|
||
|
||
int fh = FileOpen(filename, FILE_READ | FILE_BIN | FILE_COMMON);
|
||
if(fh == INVALID_HANDLE) return false;
|
||
bool ok = Load(fh);
|
||
FileClose(fh);
|
||
return ok;
|
||
}
|
||
|
||
bool Load(int fh) {
|
||
if(fh == INVALID_HANDLE) return false;
|
||
|
||
int version = FileReadInteger(fh);
|
||
int inputs = FileReadInteger(fh);
|
||
int hidden = FileReadInteger(fh);
|
||
int outputs = FileReadInteger(fh);
|
||
|
||
Init(inputs, hidden, outputs);
|
||
|
||
m_epochsTrained = FileReadInteger(fh);
|
||
m_lastLoss = FileReadDouble(fh);
|
||
if(version >= 2) m_l2 = FileReadDouble(fh);
|
||
if(version >= 3) {
|
||
m_dropoutRate = FileReadDouble(fh);
|
||
m_bnMomentum = FileReadDouble(fh);
|
||
m_bnEps = FileReadDouble(fh);
|
||
}
|
||
|
||
for(int r = 0; r < m_inputs; r++)
|
||
for(int c = 0; c < m_hidden; c++)
|
||
m_W1[r][c] = FileReadDouble(fh);
|
||
|
||
for(int i = 0; i < m_hidden; i++)
|
||
m_b1[i] = FileReadDouble(fh);
|
||
|
||
for(int r = 0; r < m_hidden; r++)
|
||
for(int c = 0; c < m_outputs; c++)
|
||
m_W2[r][c] = FileReadDouble(fh);
|
||
|
||
for(int i = 0; i < m_outputs; i++)
|
||
m_b2[i] = FileReadDouble(fh);
|
||
|
||
if(version >= 1) {
|
||
for(int r = 0; r < m_inputs; r++)
|
||
for(int c = 0; c < m_hidden; c++) {
|
||
m_mW1[r][c] = FileReadDouble(fh);
|
||
m_vW1[r][c] = FileReadDouble(fh);
|
||
}
|
||
|
||
for(int i = 0; i < m_hidden; i++) {
|
||
m_mb1[i] = FileReadDouble(fh);
|
||
m_vb1[i] = FileReadDouble(fh);
|
||
}
|
||
|
||
for(int r = 0; r < m_hidden; r++)
|
||
for(int c = 0; c < m_outputs; c++) {
|
||
m_mW2[r][c] = FileReadDouble(fh);
|
||
m_vW2[r][c] = FileReadDouble(fh);
|
||
}
|
||
|
||
for(int i = 0; i < m_outputs; i++) {
|
||
m_mb2[i] = FileReadDouble(fh);
|
||
m_vb2[i] = FileReadDouble(fh);
|
||
}
|
||
|
||
if(version >= 1) {
|
||
m_beta1T = FileReadDouble(fh);
|
||
m_beta2T = FileReadDouble(fh);
|
||
m_t = FileReadInteger(fh);
|
||
}
|
||
|
||
// BatchNorm (v3)
|
||
if(version >= 3) {
|
||
m_bnGamma.Init(m_hidden);
|
||
m_bnBeta.Init(m_hidden);
|
||
m_bnRunningMean.Init(m_hidden);
|
||
m_bnRunningVar.Init(m_hidden);
|
||
for(int i = 0; i < m_hidden; i++) {
|
||
m_bnGamma[i] = FileReadDouble(fh);
|
||
m_bnBeta[i] = FileReadDouble(fh);
|
||
m_bnRunningMean[i] = FileReadDouble(fh);
|
||
m_bnRunningVar[i] = FileReadDouble(fh);
|
||
}
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
bool IsInitialized() const { return m_initialized; }
|
||
int EpochsTrained() const { return m_epochsTrained; }
|
||
double LastLoss() const { return m_lastLoss; }
|
||
int Inputs() const { return m_inputs; }
|
||
int Hidden() const { return m_hidden; }
|
||
int Outputs() const { return m_outputs; }
|
||
|
||
string Info() const {
|
||
if(!m_initialized) return "NN: uninitialized";
|
||
return StringFormat("NN: %d→%d→%d (epoche=%d, loss=%.6f, dropout=%.2f, BN=%s)",
|
||
m_inputs, m_hidden, m_outputs, m_epochsTrained, m_lastLoss,
|
||
m_dropoutRate, m_initialized ? "on" : "off");
|
||
}
|
||
|
||
string WeightsSummary() const {
|
||
if(!m_initialized) return "NN: uninitialized";
|
||
double w1Min = 1e99, w1Max = -1e99, w1Sum = 0;
|
||
double w2Min = 1e99, w2Max = -1e99, w2Sum = 0;
|
||
int w1Count = 0, w2Count = 0;
|
||
for(int r = 0; r < m_inputs; r++) {
|
||
for(int c = 0; c < m_hidden; c++) {
|
||
double v = m_W1[r][c];
|
||
if(v < w1Min) w1Min = v; if(v > w1Max) w1Max = v;
|
||
w1Sum += v; w1Count++;
|
||
}
|
||
}
|
||
for(int r = 0; r < m_hidden; r++) {
|
||
for(int c = 0; c < m_outputs; c++) {
|
||
double v = m_W2[r][c];
|
||
if(v < w2Min) w2Min = v; if(v > w2Max) w2Max = v;
|
||
w2Sum += v; w2Count++;
|
||
}
|
||
}
|
||
return StringFormat(" W1 [%.4f, %.4f] μ=%.4f | W2 [%.4f, %.4f] μ=%.4f",
|
||
w1Min, w1Max, (w1Count>0?w1Sum/w1Count:0),
|
||
w2Min, w2Max, (w2Count>0?w2Sum/w2Count:0));
|
||
}
|
||
};
|
||
|
||
struct NNTrainSample {
|
||
double features[NN_FEATURES];
|
||
double target[NN_TARGETS];
|
||
double weight; // peso del sample: trades con grosso impatto pesano di più
|
||
};
|
||
|
||
class NNTrainBuffer {
|
||
private:
|
||
NNTrainSample m_samples[];
|
||
int m_count;
|
||
public:
|
||
NNTrainBuffer() : m_count(0) {}
|
||
|
||
void Add(double &features[], double &target[], double weight = 1.0) {
|
||
int idx = m_count;
|
||
ArrayResize(m_samples, idx + 1);
|
||
for(int i = 0; i < NN_FEATURES; i++) m_samples[idx].features[i] = features[i];
|
||
for(int i = 0; i < NN_TARGETS; i++) m_samples[idx].target[i] = target[i];
|
||
m_samples[idx].weight = weight;
|
||
m_count++;
|
||
}
|
||
|
||
int Count() const { return m_count; }
|
||
|
||
void ToMatrices(matrix &features, matrix &targets) {
|
||
if(m_count == 0) return;
|
||
features.Init(m_count, NN_FEATURES);
|
||
targets.Init(m_count, NN_TARGETS);
|
||
for(int i = 0; i < m_count; i++) {
|
||
for(int j = 0; j < NN_FEATURES; j++)
|
||
features[i][j] = m_samples[i].features[j];
|
||
for(int j = 0; j < NN_TARGETS; j++)
|
||
targets[i][j] = m_samples[i].target[j];
|
||
}
|
||
}
|
||
|
||
vector GetWeights() {
|
||
vector w(m_count);
|
||
for(int i = 0; i < m_count; i++)
|
||
w[i] = m_samples[i].weight;
|
||
return w;
|
||
}
|
||
|
||
void Clear() {
|
||
ArrayResize(m_samples, 0);
|
||
m_count = 0;
|
||
}
|
||
|
||
void Trim(int maxSamples) {
|
||
if(m_count <= maxSamples) return;
|
||
int remove = m_count - maxSamples;
|
||
for(int i = 0; i < maxSamples; i++)
|
||
m_samples[i] = m_samples[i + remove];
|
||
ArrayResize(m_samples, maxSamples);
|
||
m_count = maxSamples;
|
||
}
|
||
|
||
// Dump training samples to CSV nella cartella Common per debug
|
||
void SaveCsv(string filename = "NN_TrainingSamples.csv") {
|
||
int fh = FileOpen(filename, FILE_TXT|FILE_WRITE|FILE_COMMON);
|
||
if(fh == INVALID_HANDLE) return;
|
||
FileWriteString(fh, "sample");
|
||
for(int f = 0; f < NN_FEATURES; f++) FileWriteString(fh, ",feat_" + (string)f);
|
||
for(int t = 0; t < NN_TARGETS; t++) FileWriteString(fh, ",target_" + (string)t);
|
||
FileWriteString(fh, ",label\r\n");
|
||
for(int i = 0; i < m_count; i++) {
|
||
string line = (string)i;
|
||
for(int f = 0; f < NN_FEATURES; f++) line += "," + StringFormat("%+.6f", m_samples[i].features[f]);
|
||
for(int t = 0; t < NN_TARGETS; t++) line += "," + StringFormat("%.0f", m_samples[i].target[t]);
|
||
// Label leggibile
|
||
if(m_samples[i].target[0] == 1) line += ",BUY";
|
||
else if(m_samples[i].target[2] == 1) line += ",SELL";
|
||
else line += ",FLAT";
|
||
line += "\r\n";
|
||
FileWriteString(fh, line);
|
||
}
|
||
FileClose(fh);
|
||
Print("Training samples saved to ", filename, " (", m_count, " samples)");
|
||
}
|
||
|
||
// Stampa statistiche riassuntive del dataset
|
||
void PrintStats() {
|
||
int buyCount = 0, sellCount = 0, flatCount = 0;
|
||
for(int i = 0; i < m_count; i++) {
|
||
if(m_samples[i].target[0] == 1) buyCount++;
|
||
else if(m_samples[i].target[2] == 1) sellCount++;
|
||
else flatCount++;
|
||
}
|
||
Print(" Dataset: ", m_count, " samples | Buy: ", buyCount,
|
||
" Sell: ", sellCount, " Flat: ", flatCount);
|
||
}
|
||
|
||
// Media feature per classe (utile per vedere se le feature discriminano)
|
||
void PrintFeatureStats() {
|
||
if(m_count < 3) { Print(" Feature stats: too few samples"); return; }
|
||
double meanFeat[NN_FEATURES] = {};
|
||
double absMean[NN_FEATURES] = {};
|
||
for(int i = 0; i < m_count; i++) {
|
||
for(int f = 0; f < NN_FEATURES; f++) {
|
||
meanFeat[f] += m_samples[i].features[f];
|
||
absMean[f] += MathAbs(m_samples[i].features[f]);
|
||
}
|
||
}
|
||
Print(" Feature means (signal strength per class):");
|
||
for(int f = 0; f < NN_FEATURES; f++) {
|
||
meanFeat[f] /= m_count;
|
||
absMean[f] /= m_count;
|
||
}
|
||
string labels[NN_FEATURES] = {"Hurst","ADX","MA","Momentum","Consensus","Hunter","Agreement","TrendStr"};
|
||
for(int f = 0; f < NN_FEATURES; f++) {
|
||
Print(" [", f, "] ", labels[f], ": μ=", StringFormat("%+.4f", meanFeat[f]),
|
||
" |avg|=", StringFormat("%.4f", absMean[f]));
|
||
}
|
||
}
|
||
};
|
||
#endif
|