Warrior_EA/AI/NeuronCPU.mqh
AnimateDread 4f28165cd3 fix: remove broken DFA optimizer, use plain gradient descent
The DFA (Direct Feedback Alignment) option was never a correct implementation:
it deterministically flipped the sign of half of all gradients based on
connection index parity, causing permanent gradient ascent for those weights
and guaranteed divergence. The backward pass was also incompatible with the
OpenCL/DirectML neuron model (layer.Total() == 1). This change removes all DFA
logic, including the enum value and `DfaFeedbackSignal` method, and replaces it
with plain gradient descent in all momentum update kernels. The `optimizer`
kernel argument is retained for binary compatibility but is no longer used.
2026-07-29 00:03:54 -04:00

167 lines
8.7 KiB
MQL5

//+------------------------------------------------------------------+
//| NeuronCPU.mqh |
//| AnimateDread |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
//| CNeuron - the plain-CPU (no DLL/OpenCL/DirectML) dense neuron, |
//| last-resort fallback tier. Needs CNeuronBase (AI\Network.mqh) |
//| and CLayer/CConnection already declared - included from |
//| Network.mqh at the exact point CNeuron used to sit, so ordering |
//| matches the original file. Extracted verbatim (SOLID cleanup) - |
//| no logic changes. |
//+------------------------------------------------------------------+
class CNeuron : public CNeuronBase
{
private:
virtual bool feedForward(CLayer *prevLayer);
virtual bool calcHiddenGradients(CLayer *&nextLayer);
virtual bool updateInputWeights(CLayer *prevLayer);
public:
CNeuron(void) {};
~CNeuron(void) { Connections.Shutdown(); }
//---
virtual bool calcOutputGradients(double targetVals);
virtual double sumDOW(CLayer *&nextLayer) ;
virtual int Type(void) const { return defNeuron; }
};
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool CNeuron::updateInputWeights(CLayer *prevLayer)
{
if(CheckPointer(prevLayer) == POINTER_INVALID)
return false;
//---
double lt = eta * sqrt(1 - pow(b2, t)) / (1 - pow(b1, t));
int total = prevLayer.Total();
for(int n = 0; n < total && !IsStopped(); n++)
{
CNeuron *neuron = prevLayer.At(n);
CConnection *con = neuron.Connections.At(m_myIndex);
if(CheckPointer(con) == POINTER_INVALID)
continue;
if(optimization == SGD)
con.weight += con.deltaWeight = (gradient != 0 ? eta * neuron.getOutputVal() * gradient : 0) + (con.deltaWeight != 0 ? alpha*con.deltaWeight : 0);
else
{
// Per-WEIGHT gradient (neuron gradient x presynaptic output), matching the SGD branch above
// and every native backend's Adam kernel (`grad = g[i] * inp` in CPU_UpdateWeightsAdam).
// Previously fed the raw neuron gradient alone, giving every input weight of a neuron an
// IDENTICAL mt/vt/delta - the weight vector could only move uniformly across all inputs,
// so this tier couldn't learn per-feature structure at all.
double g = gradient * neuron.getOutputVal();
con.mt = b1 * con.mt + (1 - b1) * g;
// Stores sqrt(...) directly into vt (not the raw second-moment estimate) to exactly match
// every native backend's Adam recursion (AI\Network.cl's UpdateWeightsAdam, WarriorDML.cpp,
// WarriorCPU.cpp) - this tier previously used the textbook raw-variance recursion instead,
// which is mathematically different (sqrt isn't linear) and would silently diverge from
// GPU/CPU-DLL-trained results if training ever fell through to this last-resort CPU tier.
con.vt = sqrt(b2 * con.vt + (1 - b2) * g * g);
con.deltaWeight = MathMax(-MAX_WEIGHT_DELTA, MathMin(MAX_WEIGHT_DELTA, lt * con.mt / (con.vt > 0 ? con.vt : lt * 10) - lt * WEIGHT_DECAY * con.weight));
// No sign-agreement gate (removed 2026-07): gating each step on agreement with the CURRENT
// sample's gradient sign rectified the one-hot softmax-CCE stream - rare large true-class
// positives (1/3 of samples), frequent small wrong-class negatives (2/3) - into a permanent
// downward ratchet on every output neuron, sinking all three logits into sigmoid saturation
// together (the all-Neutral collapse; IS error frozen at sqrt(1/3)=0.58). The stale-step
// overshoot it guarded against is covered by the MAX_WEIGHT_DELTA clip, AdamW WEIGHT_DECAY
// and shuffle-interleaved oversampling. Removed from all four backends in sync (WarriorCPU
// .cpp / WarriorDML.cpp / Network.cl mirror this).
con.weight += con.deltaWeight;
}
// Mirrors AI\Network.cl's MAX_WEIGHT clamp (see that file's Conv/LSTM Adam kernels) - without
// it a gradient spike (e.g. from class-balance oversampling replaying the same rare-class bar
// several times in a row - see Train()'s reps loop) can drive a weight to +-Infinity; the next
// Adam step then divides Infinity by Infinity (mt/vt both Inf) producing NaN, which propagates
// through every FeedForward sum that touches it and never recovers, since Adam(NaN)=NaN forever
// after. That silently freezes the whole network's output at NaN - manifesting as every bar
// classifying to whatever the "can't decide" default is (e.g. all-Neutral, 0 Buy/Sell) with no
// error ever surfaced, since NaN comparisons are simply always false.
con.weight = MathMax(-MAX_WEIGHT, MathMin(MAX_WEIGHT, con.weight));
}
if(optimization == ADAM)
t++;
//---
return true;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
double CNeuron::sumDOW(CLayer *&nextLayer)
{
double sum = 0.0;
int total = nextLayer.Total() - 1;
for(int n = 0; n < total; n++)
{
CConnection *con = Connections.At(n);
if(CheckPointer(con) == POINTER_INVALID)
continue;
double weight = con.weight;
if(weight != 0)
{
CNeuron *neuron = nextLayer.At(n);
sum += weight * neuron.gradient;
}
}
return sum;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool CNeuron::calcHiddenGradients(CLayer *&nextLayer)
{
// sumDOW * activation derivative, matching CNeuronConv::calcHiddenGradients (AI\Network.mqh) and
// the CalcHiddenGradient kernels in all three native backends. Previously routed through
// calcOutputGradients(sumDOW + outputVal), whose +-1 target clamp - added later for the OUTPUT
// layer's -1/0/1 targets - silently corrupted hidden gradients: for a hidden PRELU neuron with
// |outputVal| > 1 (routine, PRELU is unbounded above) the clamp discarded the backpropagated
// sumDOW entirely and replaced it with a spurious (+-1 - outputVal) magnitude penalty, and even
// inside [-1,1] it truncated any error signal pushing the pseudo-target past +-1.
gradient = sumDOW(nextLayer) * activationFunctionDerivative(outputVal);
return true;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool CNeuron::calcOutputGradients(double targetVal)
{
// Deliberately NOT multiplied by activationFunctionDerivative(outputVal):
// for TANH that factor is (1-out^2), which vanishes as outputVal
// approaches +-1 - exactly where this neuron needs to converge for a +-1
// target (e.g. the buy/sell extremes of a single-neuron regression head),
// stalling training right when it matters most. See the matching fix in
// AI\Network.cl / DirectML\WarriorDML.cpp / DirectML\WarriorCPU.cpp.
double delta = (targetVal > 1 ? 1 : targetVal < -1 ? -1 : targetVal) - outputVal;
gradient = delta;
return true;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool CNeuron::feedForward(CLayer *prevLayer)
{
if(CheckPointer(prevLayer) == POINTER_INVALID || prevLayer.Type() != defLayer)
return false;
//---
prevVal = outputVal;
double sum = 0.0;
int total = prevLayer.Total();
for(int n = 0; n < total && !IsStopped(); n++)
{
CNeuron *temp = prevLayer.At(n);
double val = temp.getOutputVal();
if(val != 0)
{
CConnection *con = temp.Connections.At(m_myIndex);
if(CheckPointer(con) == POINTER_INVALID)
continue;
sum += val * con.weight;
}
}
outputVal = activationFunction(MathMin(MathMax(sum, -18), 18));
//---
return true;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+