forked from animatedread/Warrior_EA
136 lines
5.7 KiB
C++
136 lines
5.7 KiB
C++
// Signal/gradient flow diagnostic for the sequence-LSTM kernels, at PRODUCTION shapes.
| |||
//
| |||
// Why this exists (separate from lstm_seq_gradcheck.cpp): that check proves the BPTT
| |||
// math is CORRECT. Correct is not the same as USABLE. A textbook-correct LSTM whose
| |||
// forget gate sits at sigmoid(0)=0.5 multiplies the cell state by 0.5 every step, so
| |||
// over a 20-bar window it retains 1e-6 of bar 0 in the forward pass and propagates
| |||
// 1e-6 of the gradient back to it. It trains to a constant and the training log shows
| |||
// a flat IS error - exactly what LSTM/HYBRID did on 2026-07-30.
| |||
//
| |||
// This measures both directions at the real shapes and sweeps the forget-gate bias:
| |||
// fwd - how much h_T moves when ONLY bar t is perturbed (memory reach)
| |||
// bwd - ||dL/dx_t|| (credit assignment reach)
| |||
//
| |||
// Build (VS x64 dev prompt, this folder):
| |||
// cl /nologo /EHsc /O2 /std:c++17 lstm_seq_flowcheck.cpp WarriorCPU.lib
| |||
#include <cstdio>
| |||
#include <cmath>
| |||
#include <vector>
| |||
#include <random>
| |||
#include "WarriorCPU.h"
| |||
| |||
static const int H = 64; // derived hidden size at H1 defaults (see ComputeLstmHiddenSize)
| |||
static const int IW = 21; // per-bar feature count for the plain-LSTM topology
| |||
static const int T = 20; // m_historyBars
| |||
| |||
int main()
| |||
{
| |||
CpuHandle h = CPU_Init(2);
| |||
const int perGate = H * (H + IW + 1);
| |||
const int wTotal = 4 * perGate;
| |||
const int bias = H + IW; // bias column within a row
| |||
| |||
int wB = CPU_BufferCreate(h, wTotal);
| |||
int inB = CPU_BufferCreate(h, T * IW);
| |||
int cgB = CPU_BufferCreate(h, T * 4 * H);
| |||
int ccB = CPU_BufferCreate(h, T * H);
| |||
int chB = CPU_BufferCreate(h, T * H);
| |||
int outB = CPU_BufferCreate(h, H);
| |||
int ogB = CPU_BufferCreate(h, H);
| |||
int wgB = CPU_BufferCreate(h, wTotal);
| |||
int igB = CPU_BufferCreate(h, T * IW);
| |||
| |||
std::mt19937 rng(1234);
| |||
// LeCun-uniform, matching CNeuronLSTMOCL::SetInputs exactly.
| |||
const double scale = 1.0 / std::sqrt((double)(H + IW) + 1.0);
| |||
std::uniform_real_distribution<double> wd(-scale, scale);
| |||
std::vector<double> w0((size_t)wTotal);
| |||
for(int i = 0; i < wTotal; i++)
| |||
w0[i] = wd(rng);
| |||
| |||
std::normal_distribution<double> xd(0.0, 1.0);
| |||
std::vector<double> x((size_t)(T * IW));
| |||
for(int i = 0; i < T * IW; i++)
| |||
x[i] = xd(rng);
| |||
| |||
std::vector<double> dL((size_t)H, 0.0);
| |||
for(int k = 0; k < H; k++)
| |||
dL[k] = ((k % 7) - 3) / 3.0; // asymmetric upstream gradient
| |||
| |||
// Must track LSTM_FORGET_BIAS_INIT in AI\Network.mqh. The sweep exists to show WHY that value is
| |||
// what it is; the verdict at the end is what makes this a regression test rather than a printout.
| |||
const double SHIPPED = 2.0;
| |||
const double MIN_REACH = 0.05; // oldest bar must retain >=5% of the newest bar's influence
| |||
double shippedFwd = 0.0, shippedBwd = 0.0;
| |||
| |||
const double fb[] = { 0.0, 1.0, SHIPPED };
| |||
std::printf("shapes: H=%d IW=%d T=%d weights=%d\n\n", H, IW, T, wTotal);
| |||
| |||
for(double b : fb)
| |||
{
| |||
std::vector<double> w = w0;
| |||
for(int hid = 0; hid < H; hid++)
| |||
w[(size_t)(0 * perGate + hid * (H + IW + 1) + bias)] = b; // gate 0 == forget
| |||
| |||
CPU_BufferWrite(h, wB, w.data(), wTotal);
| |||
CPU_BufferWrite(h, inB, x.data(), T * IW);
| |||
CPU_LSTMSeqForward(h, wB, inB, cgB, ccB, chB, outB, H, IW, T);
| |||
std::vector<double> base((size_t)H);
| |||
CPU_BufferRead(h, outB, base.data(), H);
| |||
| |||
// --- forward reach: perturb ONLY bar t, measure ||dh_T||
| |||
double fwd0 = 0.0, fwdLast = 0.0;
| |||
for(int t : {0, T - 1})
| |||
{
| |||
std::vector<double> xp = x;
| |||
for(int j = 0; j < IW; j++)
| |||
xp[(size_t)(t * IW + j)] += 0.01;
| |||
CPU_BufferWrite(h, inB, xp.data(), T * IW);
| |||
CPU_LSTMSeqForward(h, wB, inB, cgB, ccB, chB, outB, H, IW, T);
| |||
std::vector<double> pert((size_t)H);
| |||
CPU_BufferRead(h, outB, pert.data(), H);
| |||
double s = 0.0;
| |||
for(int k = 0; k < H; k++)
| |||
s += (pert[k] - base[k]) * (pert[k] - base[k]);
| |||
(t == 0 ? fwd0 : fwdLast) = std::sqrt(s);
| |||
}
| |||
| |||
// --- backward reach: ||dL/dx_t|| at the first and last step
| |||
CPU_BufferWrite(h, inB, x.data(), T * IW);
| |||
CPU_LSTMSeqForward(h, wB, inB, cgB, ccB, chB, outB, H, IW, T);
| |||
CPU_BufferWrite(h, ogB, dL.data(), H);
| |||
CPU_LSTMSeqBackward(h, wB, inB, cgB, ccB, chB, ogB, wgB, igB, H, IW, T);
| |||
std::vector<double> dX((size_t)(T * IW));
| |||
CPU_BufferRead(h, igB, dX.data(), T * IW);
| |||
double n0 = 0.0, nL = 0.0;
| |||
for(int j = 0; j < IW; j++)
| |||
{
| |||
n0 += dX[(size_t)j] * dX[(size_t)j];
| |||
nL += dX[(size_t)((T - 1) * IW + j)] * dX[(size_t)((T - 1) * IW + j)];
| |||
}
| |||
n0 = std::sqrt(n0);
| |||
nL = std::sqrt(nL);
| |||
| |||
std::printf("forget-gate bias %+.1f (sigmoid=%.3f)\n", b, 1.0 / (1.0 + std::exp(-b)));
| |||
std::printf(" fwd ||dh_T|| from bar 0 = %.3e from bar %d = %.3e ratio %.2e\n",
| |||
fwd0, T - 1, fwdLast, fwdLast > 0 ? fwd0 / fwdLast : 0.0);
| |||
std::printf(" bwd ||dL/dx_0|| = %.3e ||dL/dx_%d|| = %.3e ratio %.2e\n\n",
| |||
n0, T - 1, nL, nL > 0 ? n0 / nL : 0.0);
| |||
| |||
if(b == SHIPPED)
| |||
{
| |||
shippedFwd = fwdLast > 0 ? fwd0 / fwdLast : 0.0;
| |||
shippedBwd = nL > 0 ? n0 / nL : 0.0;
| |||
}
| |||
}
| |||
| |||
CPU_Shutdown(h);
| |||
| |||
bool ok = (shippedFwd >= MIN_REACH && shippedBwd >= MIN_REACH);
| |||
std::printf("shipped forget-gate bias %+.1f: fwd reach %.3f, bwd reach %.3f (need >= %.2f) -> %s\n",
| |||
SHIPPED, shippedFwd, shippedBwd, MIN_REACH, ok ? "PASS" : "FAIL");
| |||
if(!ok)
| |||
std::printf(" The recurrence cannot see across its own window. Expect a flat IS error and\n"
| |||
" OOS recall Neutral:100%%. Check LSTM_FORGET_BIAS_INIT and the cell-state update.\n");
| |||
return ok ? 0 : 1;
| |||
}
|