2026-08-01 11:27:28 -04:00 | | | //+------------------------------------------------------------------+
|
| | | //| NetWeights.mqh |
|
| | | //| |
|
| | | //| CNet weight operations: EMA blend, in-memory snapshot/restore, |
|
| | | //| per-layer learning report, topology contract checks. |
|
| | | //| |
|
| | | //| Included from AI\Network.mqh AFTER every class declaration - |
|
| | | //| bodies only, no declarations. Relocation is behaviour-neutral by |
|
| | | //| construction: nothing here is reachable until Network.mqh ends. |
|
| | | //+------------------------------------------------------------------+
|
| | | #ifndef WARRIOR_AI_IMPL_NETWEIGHTS_MQH
|
| | | #define WARRIOR_AI_IMPL_NETWEIGHTS_MQH
|
| | | //+------------------------------------------------------------------+
|
| | | //| See this method's declaration comment for the EMA shadow-weight |
|
| | | //| deployment rationale. |
|
| | | //+------------------------------------------------------------------+
|
| | | bool CNet::BlendWeightsFrom(CNet &live, double tau)
|
| | | {
|
| | | if(CheckPointer(layers) == POINTER_INVALID || CheckPointer(live.layers) == POINTER_INVALID)
|
| | | return false;
|
| | | //--- Skip accounting. Every branch below blends only when BOTH sides hand back a weight block, and
|
| | | //--- silently does nothing otherwise - deliberate, so a partial topology mismatch degrades instead of
|
| | | //--- corrupting unrelated layers. But "silently" made a real defect invisible for 343 eras on SP500 H1
|
| | | //--- (2026-07-31): the HYBRID shadow's LSTM layer had never run a forward pass, so its WeightsLSTM was
|
| | | //--- still NULL, getWeightsLSTM() returned 0, and the blend skipped a 24,704-weight layer on EVERY era
|
| | | //--- while reporting success. It showed up only as a shadow .nnw 791,120 bytes smaller than its live
|
| | | //--- net - exactly the LSTM weight block and its Adam moments - which nothing was watching. A skip is
|
| | | //--- never normal on a shadow cloned from live, so say so ONCE per net rather than never.
|
| | | int skipped = 0;
|
| | | int skippedLayer = -1, skippedType = 0;
|
| | | int layerTotal = MathMin(layers.Total(), live.layers.Total());
|
| | | for(int l = 0; l < layerTotal; l++)
|
| | | {
|
| | | CLayer *shadowLayer = layers.At(l);
|
| | | CLayer *liveLayer = live.layers.At(l);
|
| | | if(CheckPointer(shadowLayer) == POINTER_INVALID || CheckPointer(liveLayer) == POINTER_INVALID)
|
| | | continue;
|
| | | int neuronTotal = MathMin(shadowLayer.Total(), liveLayer.Total());
|
| | | for(int n = 0; n < neuronTotal; n++)
|
| | | {
|
| | | //--- Type() first, through the true common base (CObject) - NOT through a CNeuronBaseOCL*-typed
|
 refactor(ai): remove the DirectML/D3D12 GPU compute tier (S1.5)
Three backends left, as the operator specified: OpenCL, the CPU DLL,
and pure MQL5. CDirectMLMy was a two-tier wrapper (GPU via
WarriorDML.dll, CPU via WarriorCPU.dll) whose name only ever named the
tier being removed here; the CPU DLL tier - the one actually used on
the training machine (no OpenCL, no DirectML) - is untouched.
AI/NeuronDirectML.mqh -> AI/ComputeDll.mqh: dropped the DML_* #import
block and COMPUTE_TIER_GPU (checked first that nothing persists the
enum value and only one external site reads .Tier() - safe), collapsed
every tier==CPU?CPU_x():DML_x() ternary to a straight CPU_x() call.
Renamed CDirectMLMy->CComputeDll, InitDirectML()->InitComputeDll(),
member directml/DirectML->computeDll/ComputeDll across every AI/ file
that touched a neuron/net backend plus Topology.mqh/OnlineLearning.mqh.
NetBuild.mqh's InitComputeDll also lost the dead D3D12 error-code
switch and the now-impossible GPU-tier log branch.
Verified via per-file brace-balance diff against HEAD and a whole-repo
grep for every removed symbol (CDirectMLMy/InitDirectML/
COMPUTE_TIER_GPU/DML_*) - the only surviving hit is an intentional
historical-note comment in the new file's header.
DirectML\WarriorDML.cpp/.h and its build scripts are now orphaned C++
source, left in place pending an operator decision. Architecture docs
(AI_NETWORK.md, Warrior_EA_System_Overview.md, etc.) still describe the
4-backend/GPU-tier shape and are not updated in this pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 19:32:09 -04:00 | | | //--- pointer. On the plain-CPU tier (no OpenCL, no CPU-DLL) CNet::CNet() builds legacy
|
| | | //--- CNeuronBase-hierarchy neurons (CNeuron/CNeuronConv/
|
2026-08-01 11:27:28 -04:00 | | | //--- CNeuronPool/CNeuronLSTM), an UNRELATED class hierarchy from CNeuronBaseOCL/CNeuronConvOCL/
|
| | | //--- CNeuronLSTMOCL. Assigning one of those objects to a CNeuronBaseOCL* and calling its virtual
|
| | | //--- methods (as this used to do unconditionally) reads the wrong vtable/member layout - undefined
|
| | | //--- behaviour, not just a silent no-op, every single online-learning step on that tier.
|
| | | CObject *shadowObj = shadowLayer.At(n);
|
| | | CObject *liveObj = liveLayer.At(n);
|
| | | if(CheckPointer(shadowObj) == POINTER_INVALID || CheckPointer(liveObj) == POINTER_INVALID)
|
| | | continue;
|
| | | if(shadowObj.Type() != liveObj.Type())
|
| | | continue;
|
| | | double shadowW[], liveW[];
|
| | | switch(shadowObj.Type())
|
| | | {
|
| | | case defNeuronBaseOCL:
|
| | | {
|
| | | CNeuronBaseOCL *shadowNeuron = shadowObj;
|
| | | CNeuronBaseOCL *liveNeuron = liveObj;
|
| | | int gotShadow = shadowNeuron.getWeights(shadowW);
|
| | | int gotLive = liveNeuron.getWeights(liveW);
|
| | | if(gotShadow > 0 && gotLive > 0)
|
| | | {
|
| | | int wt = MathMin(ArraySize(shadowW), ArraySize(liveW));
|
| | | for(int wi = 0; wi < wt; wi++)
|
| | | shadowW[wi] = (1.0 - tau) * shadowW[wi] + tau * liveW[wi];
|
| | | shadowNeuron.setWeights(shadowW);
|
| | | }
|
| | | else
|
| | | //--- Only an ASYMMETRY is a defect: live has a weight block, the shadow does not.
|
| | | //--- Both-empty is normal and common - a dense layer whose successor owns the weight
|
| | | //--- matrix (see CNeuronBaseOCL::Init's numOutputs>0 guard) legitimately has none, which
|
| | | //--- is also why LayerLearningReport prints NOWEIGHTS for those layers. Warning on that
|
| | | //--- fired 5-6 times per net on every topology (2026-07-31) and was pure noise.
|
| | | if(gotLive > 0)
|
| | | {
|
| | | skipped++;
|
| | | skippedLayer = l;
|
| | | skippedType = shadowObj.Type();
|
| | | }
|
| | | }
|
| | | break;
|
| | | case defNeuronBatchNormOCL:
|
| | | {
|
| | | //--- getWeightsBN packs the outgoing dense matrix and gamma/beta/statistics into one
|
| | | //--- flat array, so the shared blend loop below needs no special case of its own.
|
| | | CNeuronBatchNormOCL *shadowBN = shadowObj;
|
| | | CNeuronBatchNormOCL *liveBN = liveObj;
|
| | | int gotShadow = shadowBN.getWeightsBN(shadowW);
|
| | | int gotLive = liveBN.getWeightsBN(liveW);
|
| | | if(gotShadow > 0 && gotLive > 0)
|
| | | {
|
| | | int wt = MathMin(ArraySize(shadowW), ArraySize(liveW));
|
| | | for(int wi = 0; wi < wt; wi++)
|
| | | shadowW[wi] = (1.0 - tau) * shadowW[wi] + tau * liveW[wi];
|
| | | shadowBN.setWeightsBN(shadowW);
|
| | | }
|
| | | else
|
| | | //--- Only an ASYMMETRY is a defect: live has a weight block, the shadow does not.
|
| | | //--- Both-empty is normal and common - a dense layer whose successor owns the weight
|
| | | //--- matrix (see CNeuronBaseOCL::Init's numOutputs>0 guard) legitimately has none, which
|
| | | //--- is also why LayerLearningReport prints NOWEIGHTS for those layers. Warning on that
|
| | | //--- fired 5-6 times per net on every topology (2026-07-31) and was pure noise.
|
| | | if(gotLive > 0)
|
| | | {
|
| | | skipped++;
|
| | | skippedLayer = l;
|
| | | skippedType = shadowObj.Type();
|
| | | }
|
| | | }
|
| | | break;
|
| | | case defNeuronConvOCL:
|
| | | {
|
| | | CNeuronConvOCL *shadowConv = shadowObj;
|
| | | CNeuronConvOCL *liveConv = liveObj;
|
| | | int gotShadow = shadowConv.getWeightsConv(shadowW);
|
| | | int gotLive = liveConv.getWeightsConv(liveW);
|
| | | if(gotShadow > 0 && gotLive > 0)
|
| | | {
|
| | | int wt = MathMin(ArraySize(shadowW), ArraySize(liveW));
|
| | | for(int wi = 0; wi < wt; wi++)
|
| | | shadowW[wi] = (1.0 - tau) * shadowW[wi] + tau * liveW[wi];
|
| | | shadowConv.setWeightsConv(shadowW);
|
| | | }
|
| | | else
|
| | | //--- Only an ASYMMETRY is a defect: live has a weight block, the shadow does not.
|
| | | //--- Both-empty is normal and common - a dense layer whose successor owns the weight
|
| | | //--- matrix (see CNeuronBaseOCL::Init's numOutputs>0 guard) legitimately has none, which
|
| | | //--- is also why LayerLearningReport prints NOWEIGHTS for those layers. Warning on that
|
| | | //--- fired 5-6 times per net on every topology (2026-07-31) and was pure noise.
|
| | | if(gotLive > 0)
|
| | | {
|
| | | skipped++;
|
| | | skippedLayer = l;
|
| | | skippedType = shadowObj.Type();
|
| | | }
|
| | | }
|
| | | break;
|
| | | case defNeuronLSTMOCL:
|
| | | {
|
| | | CNeuronLSTMOCL *shadowLstm = shadowObj;
|
| | | CNeuronLSTMOCL *liveLstm = liveObj;
|
| | | int gotShadow = shadowLstm.getWeightsLSTM(shadowW);
|
| | | int gotLive = liveLstm.getWeightsLSTM(liveW);
|
| | | //--- SELF-HEAL, 2026-07-31. EnsureShadowNet() can bootstrap the clone from
|
| | | //--- RefreshLatestSignal BEFORE the live net has run a single forward pass, and
|
| | | //--- CNeuronLSTMOCL::Save omits every LSTM buffer for a layer in that state - so the shadow
|
| | | //--- came back with no WeightsLSTM and, because only the LIVE net ever runs forward, never
|
| | | //--- got one. The blend then skipped a 24,704-weight layer on EVERY era while returning
|
| | | //--- true, for a whole 343-era run. It was visible only as a shadow .nnw 791,120 bytes
|
| | | //--- short of its live net (exactly the LSTM block plus its Adam moments), and the shadow
|
| | | //--- is what live inference and deployment read.
|
| | | //--- An EMA whose accumulator does not exist yet must seed at the FIRST observation, not
|
| | | //--- blend tau of it into freshly randomized weights - hence the outright copy.
|
| | | if(gotShadow <= 0 && gotLive > 0 && shadowLstm.AdoptShapeFrom(liveLstm))
|
| | | {
|
| | | shadowLstm.setWeightsLSTM(liveW);
|
| | | break;
|
| | | }
|
| | | if(gotShadow > 0 && gotLive > 0)
|
| | | {
|
| | | int wt = MathMin(ArraySize(shadowW), ArraySize(liveW));
|
| | | for(int wi = 0; wi < wt; wi++)
|
| | | shadowW[wi] = (1.0 - tau) * shadowW[wi] + tau * liveW[wi];
|
| | | shadowLstm.setWeightsLSTM(shadowW);
|
| | | }
|
| | | else
|
| | | //--- Only an ASYMMETRY is a defect: live has a weight block, the shadow does not.
|
| | | //--- Both-empty is normal and common - a dense layer whose successor owns the weight
|
| | | //--- matrix (see CNeuronBaseOCL::Init's numOutputs>0 guard) legitimately has none, which
|
| | | //--- is also why LayerLearningReport prints NOWEIGHTS for those layers. Warning on that
|
| | | //--- fired 5-6 times per net on every topology (2026-07-31) and was pure noise.
|
| | | if(gotLive > 0)
|
| | | {
|
| | | skipped++;
|
| | | skippedLayer = l;
|
| | | skippedType = shadowObj.Type();
|
| | | }
|
| | | }
|
| | | break;
|
| | | //--- Plain-CPU tier (no backend at all): dense/conv/pool/LSTM legacy neurons store weights
|
| | | //--- per-connection (CConnection.weight) rather than in one contiguous buffer - blend those
|
| | | //--- directly instead of silently dropping the shadow-EMA deployment on this tier.
|
| | | case defNeuron:
|
| | | case defNeuronConv:
|
| | | case defNeuronPool:
|
| | | case defNeuronLSTM:
|
| | | {
|
| | | CNeuronBase *shadowBase = shadowObj;
|
| | | CNeuronBase *liveBase = liveObj;
|
| | | CArrayCon *shadowCon = shadowBase.getConnections();
|
| | | CArrayCon *liveCon = liveBase.getConnections();
|
| | | if(CheckPointer(shadowCon) == POINTER_INVALID || CheckPointer(liveCon) == POINTER_INVALID)
|
| | | break;
|
| | | int ct = MathMin(shadowCon.Total(), liveCon.Total());
|
| | | for(int ci = 0; ci < ct; ci++)
|
| | | {
|
| | | CConnection *sc = shadowCon.At(ci);
|
| | | CConnection *lc = liveCon.At(ci);
|
| | | if(CheckPointer(sc) == POINTER_INVALID || CheckPointer(lc) == POINTER_INVALID)
|
| | | continue;
|
| | | sc.weight = (1.0 - tau) * sc.weight + tau * lc.weight;
|
| | | }
|
| | | }
|
| | | break;
|
| | | }
|
| | | }
|
| | | }
|
| | | if(skipped > 0 && !m_blendSkipLogged)
|
| | | {
|
| | | m_blendSkipLogged = true;
|
| | | Print("CNet::BlendWeightsFrom: WARNING - ", skipped, " weight block(s) could not be blended into the EMA shadow ",
|
| | | "(last: layer ", skippedLayer, ", neuron type ", skippedType, "). The shadow is what live inference and ",
|
| | | "deployment read, so those layers are NOT tracking the trained model - they keep whatever they were ",
|
| | | "initialized with. A shadow cloned from the live net should never skip; investigate rather than ignore.");
|
| | | }
|
| | | return true;
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
2026-08-22 00:30:14 -04:00 | | | //| Per-layer "is this layer actually learning?" report. |
|
2026-08-01 11:27:28 -04:00 | | | //+------------------------------------------------------------------+
|
| | | string CNet::LayerLearningReport(void)
|
| | | {
|
| | | if(CheckPointer(layers) == POINTER_INVALID)
|
| | | return "";
|
| | | int total = layers.Total();
|
| | | if(ArraySize(m_prevLayerNorm) != total)
|
| | | {
|
| | | ArrayResize(m_prevLayerNorm, total);
|
| | | ArrayInitialize(m_prevLayerNorm, -1.0);
|
| | | }
|
| | | //--- One slot per layer index, so At(l) lines up with the loop below without bookkeeping. Layers that
|
| | | //--- own no weights just keep an empty array.
|
| | | if(CheckPointer(m_prevLayerWeights) == POINTER_INVALID)
|
| | | m_prevLayerWeights = new CArrayObj();
|
| | | if(CheckPointer(m_prevLayerWeights) != POINTER_INVALID)
|
| | | while(m_prevLayerWeights.Total() < total)
|
| | | if(!m_prevLayerWeights.Add(new CArrayDouble()))
|
| | | break;
|
| | | string report = "";
|
| | | for(int l = 0; l < total; l++)
|
| | | {
|
| | | CLayer *layer = (CLayer*)layers.At(l);
|
| | | if(CheckPointer(layer) == POINTER_INVALID || layer.Total() <= 0)
|
| | | continue;
|
| | | CObject *obj = layer.At(0);
|
| | | if(CheckPointer(obj) == POINTER_INVALID)
|
| | | continue;
|
| | | double w[];
|
| | | int got = 0;
|
| | | string tag = "?";
|
| | | switch(obj.Type())
|
| | | {
|
| | | case defNeuronBaseOCL:
|
| | | {
|
| | | CNeuronBaseOCL *n = (CNeuronBaseOCL*)obj;
|
| | | got = n.getWeights(w);
|
| | | tag = "dense";
|
| | | break;
|
| | | }
|
| | | case defNeuronConvOCL:
|
| | | {
|
| | | CNeuronConvOCL *c = (CNeuronConvOCL*)obj;
|
| | | got = c.getWeightsConv(w);
|
| | | tag = "conv";
|
| | | break;
|
| | | }
|
| | | case defNeuronLSTMOCL:
|
| | | {
|
| | | CNeuronLSTMOCL *ls = (CNeuronLSTMOCL*)obj;
|
| | | got = ls.getWeightsLSTM(w);
|
| | | tag = "lstm";
|
| | | break;
|
| | | }
|
| | | case defNeuronBatchNormOCL:
|
| | | {
|
| | | CNeuronBatchNormOCL *bn = (CNeuronBatchNormOCL*)obj;
|
| | | got = bn.getWeightsBN(w);
|
| | | tag = "bn";
|
2026-08-02 08:12:47 -04:00 | | | //--- getWeightsBN packs FOUR different kinds of number into one array: the outgoing dense
|
| | | //--- matrix, the learned gamma/beta, the running mean/variance, and the Adam moment
|
| | | //--- buffers. A single norm over all of them cannot say which one is moving - and on
|
| | | //--- 2026-08-02 that ambiguity was the difference between "the weights are diverging"
|
| | | //--- (an optimizer problem) and "the running variance is tracking activations that grew"
|
| | | //--- (a scaling problem), which need opposite fixes. PAI reached bn3:465,684 with no way
|
| | | //--- to tell them apart. So break the block down: the sub-norms cost one pass and turn a
|
| | | //--- guess into a reading.
|
| | | int oCount = bn.BatchOptionsTotal();
|
| | | int wCount = got - oCount;
|
| | | if(oCount > 0 && wCount >= 0)
|
| | | {
|
 fix(diagnostics+calibration): the frozen-layer reading was a broken ruler; gate the operating point on a null of the maximum
Two defects behind the "training is highly unstable" report, from 101 eras of
SP500 H4 PAI logs. Neither was the optimizer.
1) LayerLearningReport's dW/W for BN layers divided by the WHOLE packed block.
getWeightsBN concatenates the outgoing dense matrix, gamma/beta, the running
mean/variance, the Adam moments AND BN_OPT_NX - the forward-pass scratch copy
of the normalized input. At era 101 bn1's dense matrix normed 15.1 against a
block norm of 15430.3, of which NX alone was 15429.3: the weights were 0.098%
of their own denominator, a 1022x inflation. NX is also near-constant between
era-end reports (same last forward pass), which pins the numerator down too,
so the layer read "bn1:0.000%" for 101 consecutive eras and was diagnosed as a
frozen first layer. It was the ruler that was broken. The ratio now covers
trainable parameters only (dense matrix + gamma + beta); mean/var/NX/Adam are
excluded. NX is reported separately because it is a health signal in its own
right - bn5 read nx 6.8e6 over 16 neurons, ~1.7e6 per unit against a healthy
~1.0, which is what a near-zero running variance in the denominator looks like.
NO historical dW/W reading on a bn* layer is admissible evidence that a layer
did or did not train. That includes every such claim in this repo's notes.
2) FitDirConfThreshold took a bare argmax of coverage x (precision - breakEven)
over 50 bins. Measured across 98 consecutive fits:
correlation(chosen threshold, win rate at it) = -0.056 over 0.00..0.74
win rate stdev across fits = 1.32pp
binomial SE of that win rate at ~1430 calls = 1.25pp
The correlation is zero - the margin does not rank trades - and the era-to-era
spread IS its own sampling error to within 0.07pp. So the objective was
coverage x (3.4 +/- 1.3) and the argmax over ~37 eligible bins returned
whichever bin drew the luckiest sample. The threshold teleported
0.42 -> 0.04 -> 0.74 in three eras, swinging OOS coverage 0% -> 39%, leaving
the era win rate measured on 1-5 calls and swinging 0% <-> 100%. That is the
entire reported instability.
The argmax is now adopted only if it beats a DETERMINISTIC fallback - the most
selective bin still clearing the coverage floor, chosen from the margin
distribution alone and never from a win rate - by more than a best-of-N
maximum could manage on noise, sqrt(2 ln N) standard errors. Same null-of-the-
maximum correction the deploy gate already applies to model selection.
A plain one-standard-error band was tried first and is NOT sufficient: its
edge is bestScore - bestSE, and with a 2.3pp edge against a 1.25pp SE that
edge is itself +/-50%, so the admitted set would still wander by half its own
width every era. The fallback has to be independent of the noisy quantity.
Simulated on the observed numbers: falls back every era at the current 2.3pp
edge (stable), adopts the argmax once a real edge reaches ~5pp.
NOT COMPILED - user compiles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 23:44:26 -04:00 | | | double nW = 0.0, nGamma = 0.0, nBeta = 0.0, nMean = 0.0, nVar = 0.0, nAdam = 0.0, nNx = 0.0;
|
2026-08-02 08:12:47 -04:00 | | | for(int i = 0; i < wCount; i++)
|
| | | nW += w[i] * w[i];
|
 fix(diagnostics+calibration): the frozen-layer reading was a broken ruler; gate the operating point on a null of the maximum
Two defects behind the "training is highly unstable" report, from 101 eras of
SP500 H4 PAI logs. Neither was the optimizer.
1) LayerLearningReport's dW/W for BN layers divided by the WHOLE packed block.
getWeightsBN concatenates the outgoing dense matrix, gamma/beta, the running
mean/variance, the Adam moments AND BN_OPT_NX - the forward-pass scratch copy
of the normalized input. At era 101 bn1's dense matrix normed 15.1 against a
block norm of 15430.3, of which NX alone was 15429.3: the weights were 0.098%
of their own denominator, a 1022x inflation. NX is also near-constant between
era-end reports (same last forward pass), which pins the numerator down too,
so the layer read "bn1:0.000%" for 101 consecutive eras and was diagnosed as a
frozen first layer. It was the ruler that was broken. The ratio now covers
trainable parameters only (dense matrix + gamma + beta); mean/var/NX/Adam are
excluded. NX is reported separately because it is a health signal in its own
right - bn5 read nx 6.8e6 over 16 neurons, ~1.7e6 per unit against a healthy
~1.0, which is what a near-zero running variance in the denominator looks like.
NO historical dW/W reading on a bn* layer is admissible evidence that a layer
did or did not train. That includes every such claim in this repo's notes.
2) FitDirConfThreshold took a bare argmax of coverage x (precision - breakEven)
over 50 bins. Measured across 98 consecutive fits:
correlation(chosen threshold, win rate at it) = -0.056 over 0.00..0.74
win rate stdev across fits = 1.32pp
binomial SE of that win rate at ~1430 calls = 1.25pp
The correlation is zero - the margin does not rank trades - and the era-to-era
spread IS its own sampling error to within 0.07pp. So the objective was
coverage x (3.4 +/- 1.3) and the argmax over ~37 eligible bins returned
whichever bin drew the luckiest sample. The threshold teleported
0.42 -> 0.04 -> 0.74 in three eras, swinging OOS coverage 0% -> 39%, leaving
the era win rate measured on 1-5 calls and swinging 0% <-> 100%. That is the
entire reported instability.
The argmax is now adopted only if it beats a DETERMINISTIC fallback - the most
selective bin still clearing the coverage floor, chosen from the margin
distribution alone and never from a win rate - by more than a best-of-N
maximum could manage on noise, sqrt(2 ln N) standard errors. Same null-of-the-
maximum correction the deploy gate already applies to model selection.
A plain one-standard-error band was tried first and is NOT sufficient: its
edge is bestScore - bestSE, and with a 2.3pp edge against a 1.25pp SE that
edge is itself +/-50%, so the admitted set would still wander by half its own
width every era. The fallback has to be independent of the noisy quantity.
Simulated on the observed numbers: falls back every era at the current 2.3pp
edge (stable), adopts the argmax once a real edge reaches ~5pp.
NOT COMPILED - user compiles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 23:44:26 -04:00 | | | //--- COMPACTED IN PLACE to the TRAINABLE parameters only - the outgoing dense matrix
|
| | | //--- (already at [0, wCount)) followed by gamma and beta. Everything else in the packed
|
| | | //--- block is measurement state, not something backprop moves: the running mean/variance
|
| | | //--- track the activations, the Adam moments track the gradient, and BN_OPT_NX is pure
|
| | | //--- forward-pass scratch (the normalized input, rewritten every feedForward).
|
| | | //---
|
| | | //--- This is a correctness fix to the DIAGNOSTIC, not a cosmetic one. The
|
| | | //--- generic norm/step code below divides by the norm of whatever this case leaves in w[],
|
| | | //--- and until 2026-08-17 that was the whole packed block. Measured on the shipped SP500
|
| | | //--- H4 PAI model at era 101: the dense matrix norm was 15.1 while the block norm was
|
| | | //--- 15430.3, of which BN_OPT_NX alone contributed 15429.3 - so the weight matrix was
|
| | | //--- 0.098% of its own denominator, a 1022x inflation, and NX is near-CONSTANT between
|
| | | //--- era-end reports (same last forward pass), which pins the numerator down too. The
|
| | | //--- layer read "bn1:0.000%" for 101 consecutive eras and was diagnosed as a frozen
|
| | | //--- first layer. It was the ruler that was broken, not necessarily the layer. Every
|
| | | //--- historical dW/W reading on a bn* layer in this project is contaminated the same way
|
| | | //--- and must not be quoted as evidence that a layer did or did not train.
|
| | | int keep = wCount;
|
2026-08-02 08:12:47 -04:00 | | | for(int o = 0; o < oCount; o++)
|
| | | {
|
| | | double v = w[wCount + o];
|
| | | switch(o % BN_OPT_STRIDE)
|
| | | {
|
| | | case BN_OPT_MEAN: nMean += v * v; break;
|
| | | case BN_OPT_VAR: nVar += v * v; break;
|
 fix(diagnostics+calibration): the frozen-layer reading was a broken ruler; gate the operating point on a null of the maximum
Two defects behind the "training is highly unstable" report, from 101 eras of
SP500 H4 PAI logs. Neither was the optimizer.
1) LayerLearningReport's dW/W for BN layers divided by the WHOLE packed block.
getWeightsBN concatenates the outgoing dense matrix, gamma/beta, the running
mean/variance, the Adam moments AND BN_OPT_NX - the forward-pass scratch copy
of the normalized input. At era 101 bn1's dense matrix normed 15.1 against a
block norm of 15430.3, of which NX alone was 15429.3: the weights were 0.098%
of their own denominator, a 1022x inflation. NX is also near-constant between
era-end reports (same last forward pass), which pins the numerator down too,
so the layer read "bn1:0.000%" for 101 consecutive eras and was diagnosed as a
frozen first layer. It was the ruler that was broken. The ratio now covers
trainable parameters only (dense matrix + gamma + beta); mean/var/NX/Adam are
excluded. NX is reported separately because it is a health signal in its own
right - bn5 read nx 6.8e6 over 16 neurons, ~1.7e6 per unit against a healthy
~1.0, which is what a near-zero running variance in the denominator looks like.
NO historical dW/W reading on a bn* layer is admissible evidence that a layer
did or did not train. That includes every such claim in this repo's notes.
2) FitDirConfThreshold took a bare argmax of coverage x (precision - breakEven)
over 50 bins. Measured across 98 consecutive fits:
correlation(chosen threshold, win rate at it) = -0.056 over 0.00..0.74
win rate stdev across fits = 1.32pp
binomial SE of that win rate at ~1430 calls = 1.25pp
The correlation is zero - the margin does not rank trades - and the era-to-era
spread IS its own sampling error to within 0.07pp. So the objective was
coverage x (3.4 +/- 1.3) and the argmax over ~37 eligible bins returned
whichever bin drew the luckiest sample. The threshold teleported
0.42 -> 0.04 -> 0.74 in three eras, swinging OOS coverage 0% -> 39%, leaving
the era win rate measured on 1-5 calls and swinging 0% <-> 100%. That is the
entire reported instability.
The argmax is now adopted only if it beats a DETERMINISTIC fallback - the most
selective bin still clearing the coverage floor, chosen from the margin
distribution alone and never from a win rate - by more than a best-of-N
maximum could manage on noise, sqrt(2 ln N) standard errors. Same null-of-the-
maximum correction the deploy gate already applies to model selection.
A plain one-standard-error band was tried first and is NOT sufficient: its
edge is bestScore - bestSE, and with a 2.3pp edge against a 1.25pp SE that
edge is itself +/-50%, so the admitted set would still wander by half its own
width every era. The fallback has to be independent of the noisy quantity.
Simulated on the observed numbers: falls back every era at the current 2.3pp
edge (stable), adopts the argmax once a real edge reaches ~5pp.
NOT COMPILED - user compiles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 23:44:26 -04:00 | | | case BN_OPT_NX: nNx += v * v; break;
|
| | | //--- gamma/beta are the layer's own learned parameters, so they belong in the
|
| | | //--- ratio. Compaction is safe in place: the destination advances 2 slots per 9
|
| | | //--- source slots, so it can never overtake the read cursor.
|
| | | case BN_OPT_GAMMA: nGamma += v * v; w[keep++] = v; break;
|
| | | case BN_OPT_BETA: nBeta += v * v; w[keep++] = v; break;
|
2026-08-02 08:12:47 -04:00 | | | case BN_OPT_MG:
|
| | | case BN_OPT_MB:
|
| | | case BN_OPT_VG:
|
| | | case BN_OPT_VB: nAdam += v * v; break;
|
 fix(diagnostics+calibration): the frozen-layer reading was a broken ruler; gate the operating point on a null of the maximum
Two defects behind the "training is highly unstable" report, from 101 eras of
SP500 H4 PAI logs. Neither was the optimizer.
1) LayerLearningReport's dW/W for BN layers divided by the WHOLE packed block.
getWeightsBN concatenates the outgoing dense matrix, gamma/beta, the running
mean/variance, the Adam moments AND BN_OPT_NX - the forward-pass scratch copy
of the normalized input. At era 101 bn1's dense matrix normed 15.1 against a
block norm of 15430.3, of which NX alone was 15429.3: the weights were 0.098%
of their own denominator, a 1022x inflation. NX is also near-constant between
era-end reports (same last forward pass), which pins the numerator down too,
so the layer read "bn1:0.000%" for 101 consecutive eras and was diagnosed as a
frozen first layer. It was the ruler that was broken. The ratio now covers
trainable parameters only (dense matrix + gamma + beta); mean/var/NX/Adam are
excluded. NX is reported separately because it is a health signal in its own
right - bn5 read nx 6.8e6 over 16 neurons, ~1.7e6 per unit against a healthy
~1.0, which is what a near-zero running variance in the denominator looks like.
NO historical dW/W reading on a bn* layer is admissible evidence that a layer
did or did not train. That includes every such claim in this repo's notes.
2) FitDirConfThreshold took a bare argmax of coverage x (precision - breakEven)
over 50 bins. Measured across 98 consecutive fits:
correlation(chosen threshold, win rate at it) = -0.056 over 0.00..0.74
win rate stdev across fits = 1.32pp
binomial SE of that win rate at ~1430 calls = 1.25pp
The correlation is zero - the margin does not rank trades - and the era-to-era
spread IS its own sampling error to within 0.07pp. So the objective was
coverage x (3.4 +/- 1.3) and the argmax over ~37 eligible bins returned
whichever bin drew the luckiest sample. The threshold teleported
0.42 -> 0.04 -> 0.74 in three eras, swinging OOS coverage 0% -> 39%, leaving
the era win rate measured on 1-5 calls and swinging 0% <-> 100%. That is the
entire reported instability.
The argmax is now adopted only if it beats a DETERMINISTIC fallback - the most
selective bin still clearing the coverage floor, chosen from the margin
distribution alone and never from a win rate - by more than a best-of-N
maximum could manage on noise, sqrt(2 ln N) standard errors. Same null-of-the-
maximum correction the deploy gate already applies to model selection.
A plain one-standard-error band was tried first and is NOT sufficient: its
edge is bestScore - bestSE, and with a 2.3pp edge against a 1.25pp SE that
edge is itself +/-50%, so the admitted set would still wander by half its own
width every era. The fallback has to be independent of the noisy quantity.
Simulated on the observed numbers: falls back every era at the current 2.3pp
edge (stable), adopts the argmax once a real edge reaches ~5pp.
NOT COMPILED - user compiles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 23:44:26 -04:00 | | | default: break;
|
2026-08-02 08:12:47 -04:00 | | | }
|
| | | }
|
 fix(diagnostics+calibration): the frozen-layer reading was a broken ruler; gate the operating point on a null of the maximum
Two defects behind the "training is highly unstable" report, from 101 eras of
SP500 H4 PAI logs. Neither was the optimizer.
1) LayerLearningReport's dW/W for BN layers divided by the WHOLE packed block.
getWeightsBN concatenates the outgoing dense matrix, gamma/beta, the running
mean/variance, the Adam moments AND BN_OPT_NX - the forward-pass scratch copy
of the normalized input. At era 101 bn1's dense matrix normed 15.1 against a
block norm of 15430.3, of which NX alone was 15429.3: the weights were 0.098%
of their own denominator, a 1022x inflation. NX is also near-constant between
era-end reports (same last forward pass), which pins the numerator down too,
so the layer read "bn1:0.000%" for 101 consecutive eras and was diagnosed as a
frozen first layer. It was the ruler that was broken. The ratio now covers
trainable parameters only (dense matrix + gamma + beta); mean/var/NX/Adam are
excluded. NX is reported separately because it is a health signal in its own
right - bn5 read nx 6.8e6 over 16 neurons, ~1.7e6 per unit against a healthy
~1.0, which is what a near-zero running variance in the denominator looks like.
NO historical dW/W reading on a bn* layer is admissible evidence that a layer
did or did not train. That includes every such claim in this repo's notes.
2) FitDirConfThreshold took a bare argmax of coverage x (precision - breakEven)
over 50 bins. Measured across 98 consecutive fits:
correlation(chosen threshold, win rate at it) = -0.056 over 0.00..0.74
win rate stdev across fits = 1.32pp
binomial SE of that win rate at ~1430 calls = 1.25pp
The correlation is zero - the margin does not rank trades - and the era-to-era
spread IS its own sampling error to within 0.07pp. So the objective was
coverage x (3.4 +/- 1.3) and the argmax over ~37 eligible bins returned
whichever bin drew the luckiest sample. The threshold teleported
0.42 -> 0.04 -> 0.74 in three eras, swinging OOS coverage 0% -> 39%, leaving
the era win rate measured on 1-5 calls and swinging 0% <-> 100%. That is the
entire reported instability.
The argmax is now adopted only if it beats a DETERMINISTIC fallback - the most
selective bin still clearing the coverage floor, chosen from the margin
distribution alone and never from a win rate - by more than a best-of-N
maximum could manage on noise, sqrt(2 ln N) standard errors. Same null-of-the-
maximum correction the deploy gate already applies to model selection.
A plain one-standard-error band was tried first and is NOT sufficient: its
edge is bestScore - bestSE, and with a 2.3pp edge against a 1.25pp SE that
edge is itself +/-50%, so the admitted set would still wander by half its own
width every era. The fallback has to be independent of the noisy quantity.
Simulated on the observed numbers: falls back every era at the current 2.3pp
edge (stable), adopts the argmax once a real edge reaches ~5pp.
NOT COMPILED - user compiles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 23:44:26 -04:00 | | | //--- nx is reported because it is a live health signal in its own right: it is the
|
| | | //--- NORMALIZED input, so a healthy layer sits near sqrt(neurons). The same era-101
|
| | | //--- reading had bn5 at nx 6.8e6 over 16 neurons - ~1.7e6 per unit, six orders of
|
| | | //--- magnitude past unit scale - which is what a near-zero running variance in the
|
| | | //--- denominator looks like. That is a real defect and this line is how it surfaces.
|
| | | report += StringFormat(" bn%d[W %.3g|g %.3g|b %.3g|mean %.3g|var %.3g|adam %.3g|nx %.3g]",
|
2026-08-02 08:12:47 -04:00 | | | l, MathSqrt(nW), MathSqrt(nGamma), MathSqrt(nBeta),
|
 fix(diagnostics+calibration): the frozen-layer reading was a broken ruler; gate the operating point on a null of the maximum
Two defects behind the "training is highly unstable" report, from 101 eras of
SP500 H4 PAI logs. Neither was the optimizer.
1) LayerLearningReport's dW/W for BN layers divided by the WHOLE packed block.
getWeightsBN concatenates the outgoing dense matrix, gamma/beta, the running
mean/variance, the Adam moments AND BN_OPT_NX - the forward-pass scratch copy
of the normalized input. At era 101 bn1's dense matrix normed 15.1 against a
block norm of 15430.3, of which NX alone was 15429.3: the weights were 0.098%
of their own denominator, a 1022x inflation. NX is also near-constant between
era-end reports (same last forward pass), which pins the numerator down too,
so the layer read "bn1:0.000%" for 101 consecutive eras and was diagnosed as a
frozen first layer. It was the ruler that was broken. The ratio now covers
trainable parameters only (dense matrix + gamma + beta); mean/var/NX/Adam are
excluded. NX is reported separately because it is a health signal in its own
right - bn5 read nx 6.8e6 over 16 neurons, ~1.7e6 per unit against a healthy
~1.0, which is what a near-zero running variance in the denominator looks like.
NO historical dW/W reading on a bn* layer is admissible evidence that a layer
did or did not train. That includes every such claim in this repo's notes.
2) FitDirConfThreshold took a bare argmax of coverage x (precision - breakEven)
over 50 bins. Measured across 98 consecutive fits:
correlation(chosen threshold, win rate at it) = -0.056 over 0.00..0.74
win rate stdev across fits = 1.32pp
binomial SE of that win rate at ~1430 calls = 1.25pp
The correlation is zero - the margin does not rank trades - and the era-to-era
spread IS its own sampling error to within 0.07pp. So the objective was
coverage x (3.4 +/- 1.3) and the argmax over ~37 eligible bins returned
whichever bin drew the luckiest sample. The threshold teleported
0.42 -> 0.04 -> 0.74 in three eras, swinging OOS coverage 0% -> 39%, leaving
the era win rate measured on 1-5 calls and swinging 0% <-> 100%. That is the
entire reported instability.
The argmax is now adopted only if it beats a DETERMINISTIC fallback - the most
selective bin still clearing the coverage floor, chosen from the margin
distribution alone and never from a win rate - by more than a best-of-N
maximum could manage on noise, sqrt(2 ln N) standard errors. Same null-of-the-
maximum correction the deploy gate already applies to model selection.
A plain one-standard-error band was tried first and is NOT sufficient: its
edge is bestScore - bestSE, and with a 2.3pp edge against a 1.25pp SE that
edge is itself +/-50%, so the admitted set would still wander by half its own
width every era. The fallback has to be independent of the noisy quantity.
Simulated on the observed numbers: falls back every era at the current 2.3pp
edge (stable), adopts the argmax once a real edge reaches ~5pp.
NOT COMPILED - user compiles.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 23:44:26 -04:00 | | | MathSqrt(nMean), MathSqrt(nVar), MathSqrt(nAdam), MathSqrt(nNx));
|
| | | //--- The ratio below now describes trainable parameters only. The array LENGTH changes
|
| | | //--- the first time this build runs, so the size guard on m_prevLayerWeights prints one
|
| | | //--- "n/a" step for each bn layer and is exact from the next era on.
|
| | | got = keep;
|
2026-08-02 08:12:47 -04:00 | | | }
|
2026-08-01 11:27:28 -04:00 | | | break;
|
| | | }
|
| | | case defNeuronPoolOCL:
|
| | | //--- No parameters of its own. Named anyway so the report shows the real layer order.
|
| | | report += " pool" + IntegerToString(l) + ":-";
|
| | | continue;
|
| | | default:
|
| | | continue;
|
| | | }
|
| | | if(got <= 0)
|
| | | {
|
| | | report += " " + tag + IntegerToString(l) + ":NOWEIGHTS";
|
| | | continue;
|
| | | }
|
| | | double sum = 0.0;
|
| | | for(int i = 0; i < got; i++)
|
| | | sum += w[i] * w[i];
|
| | | double norm = MathSqrt(sum);
|
| | | double prev = m_prevLayerNorm[l];
|
| | | m_prevLayerNorm[l] = norm;
|
| | | //--- |dW|: norm of the elementwise change since the last report. Only meaningful when the previous
|
| | | //--- vector has the same length (a rebuilt topology invalidates it), hence the size check.
|
| | | double stepRel = -1.0;
|
| | | CArrayDouble *prevW = (CheckPointer(m_prevLayerWeights) != POINTER_INVALID && l < m_prevLayerWeights.Total()
|
| | | ? (CArrayDouble*)m_prevLayerWeights.At(l) : NULL);
|
| | | if(CheckPointer(prevW) != POINTER_INVALID)
|
| | | {
|
| | | if(prevW.Total() == got && prev > 0.0)
|
| | | {
|
| | | double d2 = 0.0;
|
| | | for(int i = 0; i < got; i++)
|
| | | {
|
| | | double d = w[i] - prevW.At(i);
|
| | | d2 += d * d;
|
| | | }
|
| | | stepRel = MathSqrt(d2) / prev;
|
| | | }
|
| | | prevW.Clear();
|
| | | for(int i = 0; i < got; i++)
|
| | | prevW.Add(w[i]);
|
| | | }
|
| | | if(prev < 0.0)
|
| | | {
|
| | | report += " " + tag + IntegerToString(l) + ":" + DoubleToString(norm, 3) + "(init)";
|
| | | continue;
|
| | | }
|
| | | //--- Relative, so a wide layer and a narrow one are comparable at a glance. Printed as
|
| | | //--- norm(d|W| / |dW|): the FIRST is how much the length changed, the SECOND how far the vector
|
| | | //--- actually moved. Decay-only shows the two roughly EQUAL with the norm falling; a learning
|
| | | //--- layer shows the second clearly larger. See m_prevLayerWeights' declaration comment.
|
| | | double rel = (prev > 0.0 ? MathAbs(norm - prev) / prev : 0.0);
|
| | | report += " " + tag + IntegerToString(l) + ":" + DoubleToString(norm, 3) +
|
| | | "(" + DoubleToString(100.0 * rel, 3) + "%/" +
|
| | | (stepRel < 0.0 ? "n/a" : DoubleToString(100.0 * stepRel, 3) + "%") + ")";
|
| | | }
|
| | | return report;
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| Snapshot every neuron's weights into host memory (see the header |
|
| | | //| declaration). Layer-major order; one CArrayDouble per neuron. Used|
|
| | | //| as the mid-run best-era checkpoint - restored by RestoreWeights().|
|
| | | //+------------------------------------------------------------------+
|
| | | bool CNet::CaptureWeights(void)
|
| | | {
|
| | | if(CheckPointer(layers) == POINTER_INVALID)
|
| | | return false;
|
| | | if(CheckPointer(m_weightSnapshot) == POINTER_INVALID)
|
| | | {
|
| | | m_weightSnapshot = new CArrayObj();
|
| | | if(CheckPointer(m_weightSnapshot) == POINTER_INVALID)
|
| | | return false;
|
| | | }
|
| | | m_weightSnapshot.Clear(); // FreeMode deletes the previous snapshot's per-neuron arrays
|
| | | m_haveWeightSnapshot = false;
|
| | | for(int l = 0; l < layers.Total(); l++)
|
| | | {
|
| | | CLayer *layer = (CLayer*)layers.At(l);
|
| | | if(CheckPointer(layer) == POINTER_INVALID)
|
| | | return false;
|
| | | for(int n = 0; n < layer.Total(); n++)
|
| | | {
|
| | | CObject *obj = layer.At(n);
|
| | | if(CheckPointer(obj) == POINTER_INVALID)
|
| | | continue;
|
| | | double w[];
|
| | | int got = 0;
|
2026-08-25 23:16:05 -04:00 | | | //--- REVERTED 2026-08-25: an earlier version of this switch treated got<=0 from any
|
| | | //--- OCL/DLL-backed type as an unambiguous device/DLL read failure and aborted the whole
|
| | | //--- capture. That premise was wrong - it is the LIVE topology, not a rare fault, for a
|
| | | //--- defNeuronBaseOCL dense layer whose immediate successor is anything other than another
|
| | | //--- plain dense layer (AI\Impl\NetBuild.mqh's `outputs` derivation: only a dense successor
|
| | | //--- gets a nonzero numOutputs, so Init() never allocates Weights at all). With
|
| | | //--- EnableBatchNorm on (the shipped default) EVERY dense layer's successor is a BatchNorm
|
| | | //--- stage, so this fired on layer 0 of essentially every model and aborted every checkpoint
|
| | | //--- capture in production within the same session it shipped - confirmed live in
|
| | | //--- 20260825.log ("weight capture ABORTED - layer 0 neuron 0"). A correct version of that
|
| | | //--- check needs a per-type "does this neuron even own a nonzero-capacity buffer" query
|
| | | //--- (WeightsCount() exists for the base dense type, but Conv/LSTM/BN keep the answer in
|
| | | //--- DIFFERENT members and were not verified here) - left for a dedicated pass rather than
|
| | | //--- guessed at again under the same time pressure that produced this bug.
|
2026-08-01 11:27:28 -04:00 | | | switch(obj.Type())
|
| | | {
|
| | | case defNeuronBaseOCL:
|
| | | {
|
| | | CNeuronBaseOCL *neuron = (CNeuronBaseOCL*)obj;
|
| | | got = neuron.getWeights(w);
|
| | | break;
|
| | | }
|
| | | case defNeuronBatchNormOCL:
|
| | | {
|
| | | //--- Dense matrix + gamma/beta/statistics as one array - see getWeightsBN. Without this
|
| | | //--- the plateau ladder would restore the weights around this layer while leaving its
|
| | | //--- own parameters at whatever the diverged era left behind.
|
| | | CNeuronBatchNormOCL *bn = (CNeuronBatchNormOCL*)obj;
|
| | | got = bn.getWeightsBN(w);
|
| | | break;
|
| | | }
|
| | | case defNeuronConvOCL:
|
| | | {
|
| | | CNeuronConvOCL *c = (CNeuronConvOCL*)obj;
|
| | | got = c.getWeightsConv(w);
|
| | | break;
|
| | | }
|
| | | case defNeuronLSTMOCL:
|
| | | {
|
| | | CNeuronLSTMOCL *ls = (CNeuronLSTMOCL*)obj;
|
| | | got = ls.getWeightsLSTM(w);
|
| | | break;
|
| | | }
|
| | | case defNeuron:
|
| | | case defNeuronConv:
|
| | | case defNeuronPool:
|
| | | case defNeuronLSTM:
|
| | | {
|
| | | CNeuronBase *neuron = (CNeuronBase*)obj;
|
| | | CArrayCon *conns = neuron.getConnections();
|
| | | if(CheckPointer(conns) != POINTER_INVALID)
|
| | | {
|
| | | got = conns.Total();
|
| | | ArrayResize(w, got);
|
| | | for(int i = 0; i < got; i++)
|
| | | {
|
| | | CConnection *con = conns.At(i);
|
| | | w[i] = (CheckPointer(con) != POINTER_INVALID) ? con.weight : 0.0;
|
| | | }
|
| | | }
|
| | | break;
|
| | | }
|
| | | default:
|
| | | break;
|
| | | }
|
| | | CArrayDouble *snap = new CArrayDouble();
|
| | | if(CheckPointer(snap) == POINTER_INVALID)
|
| | | return false;
|
| | | if(got > 0)
|
| | | snap.AssignArray(w);
|
| | | if(!m_weightSnapshot.Add(snap))
|
| | | {
|
| | | delete snap;
|
| | | return false;
|
| | | }
|
| | | }
|
| | | }
|
| | | m_haveWeightSnapshot = true;
|
| | | return true;
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| Write the CaptureWeights() snapshot back into the live neurons IN |
|
| | | //| PLACE (setWeights - no neuron re-creation, no new device tensors).|
|
| | | //| Aborts (returning false, live weights untouched past that point) |
|
| | | //| only on a neuron-count mismatch, which never happens within a run |
|
| | | //| (fixed topology). See the header declaration for the full why. |
|
| | | //+------------------------------------------------------------------+
|
| | | bool CNet::RestoreWeights(void)
|
| | | {
|
| | | if(!m_haveWeightSnapshot || CheckPointer(m_weightSnapshot) == POINTER_INVALID || CheckPointer(layers) == POINTER_INVALID)
|
| | | return false;
|
| | | int idx = 0;
|
| | | for(int l = 0; l < layers.Total(); l++)
|
| | | {
|
| | | CLayer *layer = (CLayer*)layers.At(l);
|
| | | if(CheckPointer(layer) == POINTER_INVALID)
|
| | | return false;
|
| | | for(int n = 0; n < layer.Total(); n++)
|
| | | {
|
| | | if(idx >= m_weightSnapshot.Total())
|
| | | return false; // topology/count mismatch - stop rather than mis-map weights
|
| | | CObject *obj = layer.At(n);
|
| | | CArrayDouble *snap = m_weightSnapshot.At(idx);
|
| | | idx++;
|
| | | if(CheckPointer(obj) == POINTER_INVALID || CheckPointer(snap) == POINTER_INVALID)
|
| | | continue;
|
| | | int cnt = snap.Total();
|
| | | if(cnt <= 0)
|
| | | continue; // no weights captured for this neuron (e.g. output layer) - nothing to restore
|
| | | double w[];
|
| | | ArrayResize(w, cnt);
|
| | | for(int i = 0; i < cnt; i++)
|
| | | w[i] = snap.At(i);
|
| | | switch(obj.Type())
|
| | | {
|
| | | case defNeuronBaseOCL:
|
| | | {
|
| | | CNeuronBaseOCL *neuron = (CNeuronBaseOCL*)obj;
|
| | | neuron.setWeights(w);
|
| | | break;
|
| | | }
|
| | | case defNeuronBatchNormOCL:
|
| | | {
|
| | | CNeuronBatchNormOCL *bn = (CNeuronBatchNormOCL*)obj;
|
| | | bn.setWeightsBN(w);
|
| | | break;
|
| | | }
|
| | | case defNeuronConvOCL:
|
| | | {
|
| | | CNeuronConvOCL *c = (CNeuronConvOCL*)obj;
|
| | | c.setWeightsConv(w);
|
| | | break;
|
| | | }
|
| | | case defNeuronLSTMOCL:
|
| | | {
|
| | | CNeuronLSTMOCL *ls = (CNeuronLSTMOCL*)obj;
|
| | | ls.setWeightsLSTM(w);
|
| | | break;
|
| | | }
|
| | | case defNeuron:
|
| | | case defNeuronConv:
|
| | | case defNeuronPool:
|
| | | case defNeuronLSTM:
|
| | | {
|
| | | CNeuronBase *neuron = (CNeuronBase*)obj;
|
| | | CArrayCon *conns = neuron.getConnections();
|
| | | if(CheckPointer(conns) == POINTER_INVALID)
|
| | | break;
|
| | | int count = MathMin(cnt, conns.Total());
|
| | | for(int i = 0; i < count; i++)
|
| | | {
|
| | | CConnection *con = conns.At(i);
|
| | | if(CheckPointer(con) != POINTER_INVALID)
|
| | | con.weight = w[i];
|
| | | }
|
| | | break;
|
| | | }
|
| | | default:
|
| | | break;
|
| | | }
|
| | | }
|
| | | }
|
| | | return (idx == m_weightSnapshot.Total());
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
2026-08-22 00:30:14 -04:00 | | | //| Effective batch size - what training GETS, not what it asked |
|
| | | //| for. |
|
 feat: mini-batch gradient accumulation (F4), front-end-aware capacity budget (F6), split Wyckoff categoricals (N1)
Completes the 2026-08-09 training audit. FORCES A RETRAIN of every
Wyckoff-enabled config (N1 re-keys the fingerprint), and BOTH DLLs must be
redeployed alongside the .ex5 - they carry new exports.
F4 - mini-batch accumulation, TRAIN_BATCH_SIZE=32. Training was pure online
SGD (one weight update per bar), which is the mechanical source of the
era-to-era whipsaw every downstream guard was built to cope with. The O(n^2)
outer product is native - AccumulateWeightGrad / AccumulateWeightGradConv /
AccumulateBufferInto in Network.cl, WarriorCPU and WarriorDML - while the
optimizer step is host-side MQL5 shared by all tiers (ApplyAccumToBlock), so
there is one Adam/SGD implementation instead of four that can drift.
- the LSTM needs no outer-product kernel (WeightsGradient already holds the
sample's full dW) but could NOT simply be left un-zeroed between samples:
CPU_LSTMSeqBackward/DML_LSTMSeqBackward memset it on entry. Hence a
separate accumulator plus an elementwise add.
- batch-norm gamma/beta accumulate in host arrays, not new BatchOptions
slots - BN_OPT_STRIDE is baked into every persisted .nnw.
- scoped to pass 2; online learning keeps immediate updates. Every save /
checkpoint / scoring boundary flushes, scaling by the real sample count.
- degrades to per-sample updates (one log line) on a tier that cannot
accumulate, so old devices and DLL-free builds are unaffected.
- verified offline: DirectML/batch_accum_check.cpp drives the real exports
against an independent reference; at B=1 the accumulator matches the
shipped unbatched kernel's own gradient to 1.1e-16. Math only - the
in-situ check remains the per-layer dW/W report on a real era.
F6 - ComputeFirstLayerWidth budgeted against the RAW input width even where a
conv/LSTM front end had already reduced it, so an LSTM's dense stack was
charged for 1,280 inputs when it receives 64. Confirmed from the deployed
.cfg files: CONV, LSTM and HYBRID were all pinned at the 16-unit floor. Now
budgeted against the front-end output and capped at it (never fan out), with
the derivation reordered so both stages settle first.
N1 - EventCode/EventPhase/StructuralPhase are signed categoricals packing
direction and Wyckoff stage into one scalar across a sign discontinuity. Split
into direction + [0,1] magnitude, the same convention the base OHLC block uses.
Information-preserving; 13 readings now occupy 16 inputs.
Compiled clean (0 errors, 0 warnings); both DLLs rebuilt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 11:48:03 -04:00 | | | //+------------------------------------------------------------------+
|
| | | int CNet::BatchSize(void)
|
| | | {
|
| | | if(m_batchSizeRequested <= 1)
|
| | | return 1;
|
| | | bool canBatch = (CheckPointer(opencl) != POINTER_INVALID && m_batchKernelsOk) ||
|
 refactor(ai): remove the DirectML/D3D12 GPU compute tier (S1.5)
Three backends left, as the operator specified: OpenCL, the CPU DLL,
and pure MQL5. CDirectMLMy was a two-tier wrapper (GPU via
WarriorDML.dll, CPU via WarriorCPU.dll) whose name only ever named the
tier being removed here; the CPU DLL tier - the one actually used on
the training machine (no OpenCL, no DirectML) - is untouched.
AI/NeuronDirectML.mqh -> AI/ComputeDll.mqh: dropped the DML_* #import
block and COMPUTE_TIER_GPU (checked first that nothing persists the
enum value and only one external site reads .Tier() - safe), collapsed
every tier==CPU?CPU_x():DML_x() ternary to a straight CPU_x() call.
Renamed CDirectMLMy->CComputeDll, InitDirectML()->InitComputeDll(),
member directml/DirectML->computeDll/ComputeDll across every AI/ file
that touched a neuron/net backend plus Topology.mqh/OnlineLearning.mqh.
NetBuild.mqh's InitComputeDll also lost the dead D3D12 error-code
switch and the now-impossible GPU-tier log branch.
Verified via per-file brace-balance diff against HEAD and a whole-repo
grep for every removed symbol (CDirectMLMy/InitDirectML/
COMPUTE_TIER_GPU/DML_*) - the only surviving hit is an intentional
historical-note comment in the new file's header.
DirectML\WarriorDML.cpp/.h and its build scripts are now orphaned C++
source, left in place pending an operator decision. Architecture docs
(AI_NETWORK.md, Warrior_EA_System_Overview.md, etc.) still describe the
4-backend/GPU-tier shape and are not updated in this pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 19:32:09 -04:00 | | | CheckPointer(computeDll) != POINTER_INVALID;
|
 feat: mini-batch gradient accumulation (F4), front-end-aware capacity budget (F6), split Wyckoff categoricals (N1)
Completes the 2026-08-09 training audit. FORCES A RETRAIN of every
Wyckoff-enabled config (N1 re-keys the fingerprint), and BOTH DLLs must be
redeployed alongside the .ex5 - they carry new exports.
F4 - mini-batch accumulation, TRAIN_BATCH_SIZE=32. Training was pure online
SGD (one weight update per bar), which is the mechanical source of the
era-to-era whipsaw every downstream guard was built to cope with. The O(n^2)
outer product is native - AccumulateWeightGrad / AccumulateWeightGradConv /
AccumulateBufferInto in Network.cl, WarriorCPU and WarriorDML - while the
optimizer step is host-side MQL5 shared by all tiers (ApplyAccumToBlock), so
there is one Adam/SGD implementation instead of four that can drift.
- the LSTM needs no outer-product kernel (WeightsGradient already holds the
sample's full dW) but could NOT simply be left un-zeroed between samples:
CPU_LSTMSeqBackward/DML_LSTMSeqBackward memset it on entry. Hence a
separate accumulator plus an elementwise add.
- batch-norm gamma/beta accumulate in host arrays, not new BatchOptions
slots - BN_OPT_STRIDE is baked into every persisted .nnw.
- scoped to pass 2; online learning keeps immediate updates. Every save /
checkpoint / scoring boundary flushes, scaling by the real sample count.
- degrades to per-sample updates (one log line) on a tier that cannot
accumulate, so old devices and DLL-free builds are unaffected.
- verified offline: DirectML/batch_accum_check.cpp drives the real exports
against an independent reference; at B=1 the accumulator matches the
shipped unbatched kernel's own gradient to 1.1e-16. Math only - the
in-situ check remains the per-layer dW/W report on a real era.
F6 - ComputeFirstLayerWidth budgeted against the RAW input width even where a
conv/LSTM front end had already reduced it, so an LSTM's dense stack was
charged for 1,280 inputs when it receives 64. Confirmed from the deployed
.cfg files: CONV, LSTM and HYBRID were all pinned at the 16-unit floor. Now
budgeted against the front-end output and capped at it (never fan out), with
the derivation reordered so both stages settle first.
N1 - EventCode/EventPhase/StructuralPhase are signed categoricals packing
direction and Wyckoff stage into one scalar across a sign discontinuity. Split
into direction + [0,1] magnitude, the same convention the base OHLC block uses.
Information-preserving; 13 readings now occupy 16 inputs.
Compiled clean (0 errors, 0 warnings); both DLLs rebuilt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 11:48:03 -04:00 | | | if(!canBatch)
|
| | | {
|
| | | if(!m_batchWarned)
|
| | | {
|
| | | m_batchWarned = true;
|
| | | Print("CNet::BatchSize: mini-batch training was requested (size ", m_batchSizeRequested,
|
| | | ") but this compute tier cannot accumulate gradients - falling back to one weight update ",
|
| | | "per sample. Training is correct, just noisier; see the F4 note in ",
|
| | | "research\\training_pipeline_audit_2026-08-09.md.");
|
| | | }
|
| | | return 1;
|
| | | }
|
| | | return m_batchSizeRequested;
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| Zero every accumulator, opening a fresh batch. |
|
| | | //+------------------------------------------------------------------+
|
| | | bool CNet::BeginBatch(void)
|
| | | {
|
| | | if(CheckPointer(layers) == POINTER_INVALID)
|
| | | return false;
|
| | | bool ok = true;
|
| | | for(int l = 0; l < layers.Total(); l++)
|
| | | {
|
| | | CLayer *layer = (CLayer*)layers.At(l);
|
| | | if(CheckPointer(layer) == POINTER_INVALID)
|
| | | continue;
|
| | | for(int n = 0; n < layer.Total(); n++)
|
| | | {
|
| | | CObject *obj = layer.At(n);
|
| | | if(CheckPointer(obj) == POINTER_INVALID)
|
| | | continue;
|
| | | switch(obj.Type())
|
| | | {
|
| | | case defNeuronBaseOCL:
|
| | | case defNeuronBatchNormOCL:
|
| | | case defNeuronConvOCL:
|
| | | case defNeuronPoolOCL:
|
| | | case defNeuronLSTMOCL:
|
| | | {
|
| | | CNeuronBaseOCL *neuron = (CNeuronBaseOCL*)obj;
|
| | | ok = neuron.BeginGradAccum() && ok;
|
| | | break;
|
| | | }
|
| | | default:
|
| | | break;
|
| | | }
|
| | | }
|
| | | }
|
| | | m_batchCount = 0;
|
| | | m_batchBegun = true;
|
| | | return ok;
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
2026-08-22 00:30:14 -04:00 | | | //| Apply whatever the current (possibly partial) batch accumulated, |
|
| | | //| then close it. Safe and cheap to call when nothing is pending - |
|
| | | //| every save/checkpoint/scoring boundary calls it unconditionally. |
|
 feat: mini-batch gradient accumulation (F4), front-end-aware capacity budget (F6), split Wyckoff categoricals (N1)
Completes the 2026-08-09 training audit. FORCES A RETRAIN of every
Wyckoff-enabled config (N1 re-keys the fingerprint), and BOTH DLLs must be
redeployed alongside the .ex5 - they carry new exports.
F4 - mini-batch accumulation, TRAIN_BATCH_SIZE=32. Training was pure online
SGD (one weight update per bar), which is the mechanical source of the
era-to-era whipsaw every downstream guard was built to cope with. The O(n^2)
outer product is native - AccumulateWeightGrad / AccumulateWeightGradConv /
AccumulateBufferInto in Network.cl, WarriorCPU and WarriorDML - while the
optimizer step is host-side MQL5 shared by all tiers (ApplyAccumToBlock), so
there is one Adam/SGD implementation instead of four that can drift.
- the LSTM needs no outer-product kernel (WeightsGradient already holds the
sample's full dW) but could NOT simply be left un-zeroed between samples:
CPU_LSTMSeqBackward/DML_LSTMSeqBackward memset it on entry. Hence a
separate accumulator plus an elementwise add.
- batch-norm gamma/beta accumulate in host arrays, not new BatchOptions
slots - BN_OPT_STRIDE is baked into every persisted .nnw.
- scoped to pass 2; online learning keeps immediate updates. Every save /
checkpoint / scoring boundary flushes, scaling by the real sample count.
- degrades to per-sample updates (one log line) on a tier that cannot
accumulate, so old devices and DLL-free builds are unaffected.
- verified offline: DirectML/batch_accum_check.cpp drives the real exports
against an independent reference; at B=1 the accumulator matches the
shipped unbatched kernel's own gradient to 1.1e-16. Math only - the
in-situ check remains the per-layer dW/W report on a real era.
F6 - ComputeFirstLayerWidth budgeted against the RAW input width even where a
conv/LSTM front end had already reduced it, so an LSTM's dense stack was
charged for 1,280 inputs when it receives 64. Confirmed from the deployed
.cfg files: CONV, LSTM and HYBRID were all pinned at the 16-unit floor. Now
budgeted against the front-end output and capped at it (never fan out), with
the derivation reordered so both stages settle first.
N1 - EventCode/EventPhase/StructuralPhase are signed categoricals packing
direction and Wyckoff stage into one scalar across a sign discontinuity. Split
into direction + [0,1] magnitude, the same convention the base OHLC block uses.
Information-preserving; 13 readings now occupy 16 inputs.
Compiled clean (0 errors, 0 warnings); both DLLs rebuilt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 11:48:03 -04:00 | | | //+------------------------------------------------------------------+
|
| | | bool CNet::FlushBatch(void)
|
| | | {
|
| | | if(m_batchCount <= 0 || CheckPointer(layers) == POINTER_INVALID)
|
| | | {
|
| | | m_batchCount = 0;
|
| | | m_batchBegun = false;
|
| | | return true;
|
| | | }
|
| | | double scale = 1.0 / (double)m_batchCount;
|
| | | bool ok = true;
|
| | | for(int l = 0; l < layers.Total(); l++)
|
| | | {
|
| | | CLayer *layer = (CLayer*)layers.At(l);
|
| | | if(CheckPointer(layer) == POINTER_INVALID)
|
| | | continue;
|
| | | for(int n = 0; n < layer.Total(); n++)
|
| | | {
|
| | | CObject *obj = layer.At(n);
|
| | | if(CheckPointer(obj) == POINTER_INVALID)
|
| | | continue;
|
| | | switch(obj.Type())
|
| | | {
|
| | | case defNeuronBaseOCL:
|
| | | case defNeuronBatchNormOCL:
|
| | | case defNeuronConvOCL:
|
| | | case defNeuronPoolOCL:
|
| | | case defNeuronLSTMOCL:
|
| | | {
|
| | | CNeuronBaseOCL *neuron = (CNeuronBaseOCL*)obj;
|
| | | ok = neuron.ApplyAccumulatedGradients(scale) && ok;
|
| | | break;
|
| | | }
|
| | | default:
|
| | | break;
|
| | | }
|
| | | }
|
| | | }
|
| | | m_batchCount = 0;
|
| | | m_batchBegun = false;
|
| | | return ok;
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
 fix: training-stability audit fixes F1/F2/F3/F5 - unbiased shuffle, real plateau escapes, fresh optimizer state on restore, pure OOS metric
Four of the six findings from research/training_pipeline_audit_2026-08-09.md
(F4 mini-batching and F6 feature re-encode deliberately deferred - see the
report's implementation-status section for why):
- F1: pass-2 Fisher-Yates (and AutoTune's MI block shuffle) used MathRand()%,
which is 15-bit - provably non-uniform on every full-history era over 32,768
queued samples. New 30-bit ShuffleRandomIndex().
- F2: plateau warm restarts were a no-op whenever eta already sat at its
ceiling (the normal state of a non-regressing plateau) - the ladder was just
a 24-era countdown. Restarts now overshoot to 5x the ceiling
(PLATEAU_RESTART_BOOST) and anneal geometrically back over the patience
window, SGDR-style; ETA_MIN widened 1e-4 -> 1e-5 so the decay schedule has
real range.
- F3: checkpoint restores put weights back but kept the rejected trajectory's
Adam moments, so the optimizer immediately pushed back toward the rolled-back
state (the restore->regress->restore oscillation). CNet::ResetOptimizerState()
zeroes moments/momentum/step counters (weights, BN statistics, gamma/beta
untouched) on every mid-run restore, every boosted restart, and the
deploy-time restore that online learning continues from.
- F5: batch-norm running statistics now freeze for the pass-3 OOS scoring walk,
so the selection metric the checkpoint ranking and deploy gate read is a pure
function of the checkpoint instead of partly measuring BN drift. Defensive
unfreeze in FinalizeTrainRun covers stop-mid-pass; live/online adaptation and
the OOS continual-learning simulation stay adaptive by design.
Compiled clean (0 errors, 0 warnings) via the staged-tree recipe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 10:54:09 -04:00 | | | //| Zero every neuron's optimizer state (Adam m/v, SGD momentum, step |
|
| | | //| counters, batch-norm gamma/beta moments), weights untouched - see |
|
| | | //| the declaration comment for the restore/warm-restart rationale. |
|
| | | //| Traversal mirrors SetBatchNormFrozen; per-type specifics live in |
|
| | | //| each neuron class's own ResetOptimizerState override. |
|
| | | //+------------------------------------------------------------------+
|
| | | bool CNet::ResetOptimizerState(void)
|
| | | {
|
| | | if(CheckPointer(layers) == POINTER_INVALID)
|
| | | return false;
|
| | | bool ok = true;
|
| | | for(int l = 0; l < layers.Total(); l++)
|
| | | {
|
| | | CLayer *layer = (CLayer*)layers.At(l);
|
| | | if(CheckPointer(layer) == POINTER_INVALID)
|
| | | continue;
|
| | | for(int n = 0; n < layer.Total(); n++)
|
| | | {
|
| | | //--- Type() first through CObject, then cast into the right hierarchy - the OCL and legacy
|
| | | //--- scalar neuron families are unrelated classes, exactly as in BlendWeightsFrom above.
|
| | | CObject *obj = layer.At(n);
|
| | | if(CheckPointer(obj) == POINTER_INVALID)
|
| | | continue;
|
| | | switch(obj.Type())
|
| | | {
|
| | | case defNeuronBaseOCL:
|
| | | case defNeuronBatchNormOCL:
|
| | | case defNeuronConvOCL:
|
| | | case defNeuronPoolOCL:
|
| | | case defNeuronLSTMOCL:
|
| | | {
|
| | | CNeuronBaseOCL *neuron = (CNeuronBaseOCL*)obj;
|
| | | ok = neuron.ResetOptimizerState() && ok;
|
| | | break;
|
| | | }
|
| | | case defNeuron:
|
| | | case defNeuronConv:
|
| | | case defNeuronPool:
|
| | | case defNeuronLSTM:
|
| | | {
|
| | | CNeuronBase *neuron = (CNeuronBase*)obj;
|
| | | ok = neuron.ResetOptimizerState() && ok;
|
| | | break;
|
| | | }
|
| | | default:
|
| | | break;
|
| | | }
|
| | | }
|
| | | }
|
| | | return ok;
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
2026-08-01 11:27:28 -04:00 | | | //| See this method's declaration comment for the cold-start bias |
|
| | | //| rationale. The weight block that produces the output layer's |
|
| | | //| values is stored on the layer BEFORE it (see CNeuronBaseOCL:: |
|
| | | //| feedForward(CNeuronBaseOCL*) - matrix_w comes from the SOURCE |
|
| | | //| neuron, laid out as (sourceNeurons+1) values per destination |
|
| | | //| neuron, the last of which is that neuron's bias term), so this |
|
| | | //| reaches one layer back from the output layer to edit it. |
|
| | | //+------------------------------------------------------------------+
|
| | | bool CNet::SeedOutputLayerBias(const double &biasValues[])
|
| | | {
|
| | | int outputs = ArraySize(biasValues);
|
| | | if(outputs <= 0 || CheckPointer(layers) == POINTER_INVALID || layers.Total() < 2)
|
| | | return false;
|
| | | CLayer *sourceLayer = layers.At(layers.Total() - 2);
|
| | | if(CheckPointer(sourceLayer) == POINTER_INVALID || sourceLayer.Total() <= 0)
|
| | | return false;
|
| | | CObject *sourceObj = sourceLayer.At(0);
|
| | | //--- Batch norm is accepted here as well as a plain dense layer. It is a CNeuronBaseOCL subclass
|
| | | //--- with the identical (inputs+1)*outputs weight layout - it just happens to be the layer that
|
| | | //--- carries the head's weight matrix once normalization is enabled (see AI\NeuronBatchNorm.mqh and
|
| | | //--- BuildFreshTopology's batch-norm insertion). Without this the exact-type check silently failed
|
| | | //--- and the cold-start output-bias seed stopped being applied for every batch-norm topology.
|
| | | if(CheckPointer(sourceObj) == POINTER_INVALID ||
|
| | | (sourceObj.Type() != defNeuronBaseOCL && sourceObj.Type() != defNeuronBatchNormOCL))
|
| | | return false;
|
| | | CNeuronBaseOCL *sourceNeuron = (CNeuronBaseOCL*)sourceObj;
|
| | | if(CheckPointer(sourceNeuron) == POINTER_INVALID)
|
| | | return false;
|
| | | int inputs = sourceNeuron.Neurons();
|
| | | double weights[];
|
| | | int count = sourceNeuron.getWeights(weights);
|
| | | if(count != (inputs + 1) * outputs)
|
| | | return false; // layout doesn't match the assumed dense (source+1)*outputs block - don't guess
|
| | | for(int i = 0; i < outputs; i++)
|
| | | weights[(inputs + 1) * i + inputs] = biasValues[i];
|
| | | return sourceNeuron.setWeights(weights);
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| |
|
| | | //+------------------------------------------------------------------+
|
| | | void CNet::SetBatchNormFrozen(bool frozen)
|
| | | {
|
| | | if(CheckPointer(layers) == POINTER_INVALID)
|
| | | return;
|
| | | for(int l = 0; l < layers.Total(); l++)
|
| | | {
|
| | | CLayer *layer = (CLayer*)layers.At(l);
|
| | | if(CheckPointer(layer) == POINTER_INVALID)
|
| | | continue;
|
| | | for(int n = 0; n < layer.Total(); n++)
|
| | | {
|
| | | CObject *obj = layer.At(n);
|
| | | if(CheckPointer(obj) == POINTER_INVALID || obj.Type() != defNeuronBatchNormOCL)
|
| | | continue;
|
| | | CNeuronBatchNormOCL *bn = (CNeuronBatchNormOCL*)obj;
|
| | | bn.SetStatsFrozen(frozen);
|
| | | }
|
| | | }
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
 feat(hud): per-member neuron lines + a vote label that moves as the nets learn
Both 2026-08-19 reports were the same staleness: every source behind the
label was an ERA artifact (live cache refills at pass-3 completion, the
snapshot copies once per era, dPrevSignal is the frozen purge-band edge
bar) - so the readout stepped at era cadence at best, stayed glued to
one direction, and lagged the era counter.
DisplayInference(): throttled (4s, 1s across an era boundary),
SIDE-EFFECT-FREE forward of the current decision bar (window ending on
bar 1, same question the live path asks) through the LEARNER net.
Batch-norm running stats are bracketed frozen/RESTORED via the new
CNet::GetBatchNormFrozen() + CNeuronBatchNormOCL::StatsFrozen() - restore,
not unfreeze, because a display tick can land between pass-3 chunks whose
whole scan holds them frozen. Writes nothing a trading or training path
reads (dPrevSignal, NMS state, tallies, watermarks all untouched;
RefreshLatestSignal is not reusable here precisely because it writes all
of them). LSTM safe by construction: h/c zeroed per forward.
ProspectiveVote() reads the fresh forward as its FIRST source; the
era-artifact chain becomes the fallback (meta head, warm-up, window
holes).
DisplayHudLine(): the reference library's training label, per ensemble
member - name, output activations (softmax probs or raw scalar), the
decision, its weighted vote (the exact consensus numerator term), era,
recent average error, "(trn)" while not vote-capable. Rendered under the
vote line in RefreshVoteReadout BEFORE the live-vote defer (member lines
are telemetry, not tradable readings), coloured by the member's own
direction in muted tones - the vote line's strict
green-only-when-it-would-trade rule is untouched.
NOT COMPILED - user compiles in MetaEditor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 08:48:34 -04:00 | | | //| See the declaration note: read-back so callers can save/restore. |
|
| | | //+------------------------------------------------------------------+
|
| | | bool CNet::GetBatchNormFrozen(void)
|
| | | {
|
| | | if(CheckPointer(layers) == POINTER_INVALID)
|
| | | return false;
|
| | | for(int l = 0; l < layers.Total(); l++)
|
| | | {
|
| | | CLayer *layer = (CLayer*)layers.At(l);
|
| | | if(CheckPointer(layer) == POINTER_INVALID)
|
| | | continue;
|
| | | for(int n = 0; n < layer.Total(); n++)
|
| | | {
|
| | | CObject *obj = layer.At(n);
|
| | | if(CheckPointer(obj) == POINTER_INVALID || obj.Type() != defNeuronBatchNormOCL)
|
| | | continue;
|
| | | CNeuronBatchNormOCL *bn = (CNeuronBatchNormOCL*)obj;
|
| | | return bn.StatsFrozen();
|
| | | }
|
| | | }
|
| | | return false;
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
2026-08-22 00:30:14 -04:00 | | | //| Window of the FIRST convolutional layer in the loaded net, or 0 |
|
| | | //| when there is none. |
|
2026-08-01 11:27:28 -04:00 | | | //+------------------------------------------------------------------+
|
| | | uint CNet::FirstConvWindow(void)
|
| | | {
|
| | | if(CheckPointer(layers) == POINTER_INVALID)
|
| | | return 0;
|
| | | for(int i = 0; i < layers.Total(); i++)
|
| | | {
|
| | | CLayer *layer = layers.At(i);
|
| | | if(CheckPointer(layer) == POINTER_INVALID || layer.Total() <= 0)
|
| | | continue;
|
| | | CObject *obj = layer.At(0);
|
| | | if(CheckPointer(obj) == POINTER_INVALID)
|
| | | continue;
|
| | | if(obj.Type() == defNeuronConvOCL)
|
| | | {
|
| | | CNeuronConvOCL *conv = (CNeuronConvOCL*)obj;
|
| | | return conv.Window();
|
| | | }
|
| | | }
|
| | | return 0;
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
| | | //| See the declaration comment for why a loaded model's activation |
|
| | | //| cannot be trusted and must be re-asserted from the topology spec. |
|
| | | //+------------------------------------------------------------------+
|
| | | bool CNet::EnforceOutputActivation(ENUM_ACTIVATION intended, ENUM_ACTIVATION &previous)
|
| | | {
|
| | | previous = intended;
|
| | | if(CheckPointer(layers) == POINTER_INVALID || layers.Total() < 2)
|
| | | return false;
|
| | | CLayer *outputLayer = layers.At(layers.Total() - 1);
|
| | | if(CheckPointer(outputLayer) == POINTER_INVALID || outputLayer.Total() <= 0)
|
| | | return false;
|
| | | CObject *obj = outputLayer.At(0);
|
| | | if(CheckPointer(obj) == POINTER_INVALID)
|
| | | return false;
|
| | | //--- Both neuron models expose the same two accessors (CNeuronBase gained Activation() for exactly
|
| | | //--- this), so one branch per family is enough - and anything else (conv/pool/LSTM can never be an
|
| | | //--- output layer in this project's topologies) is left untouched rather than force-cast.
|
| | | int t = obj.Type();
|
| | | if(t == defNeuronBaseOCL || t == defNeuronConvOCL || t == defNeuronPoolOCL || t == defNeuronLSTMOCL ||
|
| | | t == defNeuronBatchNormOCL)
|
| | | {
|
| | | CNeuronBaseOCL *n = (CNeuronBaseOCL*)obj;
|
| | | previous = n.Activation();
|
| | | if(previous == intended)
|
| | | return false;
|
| | | n.SetActivationFunction(intended);
|
| | | return true;
|
| | | }
|
| | | if(t == defNeuron || t == defNeuronConv || t == defNeuronPool || t == defNeuronLSTM)
|
| | | {
|
| | | //--- The scalar CPU model stores activation per NEURON, not per layer, so every neuron in the
|
| | | //--- output layer has to be corrected - not just index 0.
|
| | | bool repaired = false;
|
| | | for(int i = 0; i < outputLayer.Total(); i++)
|
| | | {
|
| | | CObject *cell = outputLayer.At(i);
|
| | | if(CheckPointer(cell) == POINTER_INVALID || cell.Type() != defNeuron)
|
| | | continue;
|
| | | CNeuronBase *n = (CNeuronBase*)cell;
|
| | | if(n.Activation() == intended)
|
| | | continue;
|
| | | if(!repaired)
|
| | | previous = n.Activation();
|
| | | n.SetActivationFunction(intended);
|
| | | repaired = true;
|
| | | }
|
| | | return repaired;
|
| | | }
|
| | | return false;
|
| | | }
|
| | | #endif
|