89370008da
The gun has never contributed anything: Tsetlin.vHits was byte-for-byte equal to Linear.vHits in every measured round of every run, because its learned correction was always exactly 0. Six diagnosed defects fixed, plus one that was required to make the first one work: 1. Type I now conditions on the clause output. It previously rewarded included true literals unconditionally, omitting Granmo's (c=0, lk=1) -> toward Exclude counter-force, so true literals ratcheted toward Include forever. This was the root cause of the saturation. 2. Type II was unreachable dead code: its guard required cOut==1 AND lits[lit]==0 AND st>0 (included), but cOut==1 guarantees every included literal is 1. Its direction was wrong too - it should increment EXCLUDED false literals when the clause fires. 3. Resource allocation restored: Granmo's (T - clip(v,-T,T))/(2T) target replaces |error|/(2*RESID_MAX); TM_T was only an output normaliser. 4. Label baseline fixed - the factor-2 shrink. predX = linearX + cx, so the label was delta - cx while the learner's output IS cx, giving error = delta - 2cx and a fixed point of cx = delta/2: HALF the needed correction even with perfect feedback. TmTrace now stores linearX/linearY and training uses delta. 5. Hits no longer zero their label (a hit means |miss| < 18px, not 0). 6. The enemy-energy feature was duplicated - tmEncodeFrame passed state.selfEnergy with a stale comment claiming enemyEnergy was absent, while WorldState.enemyEnergy exists. Enemy-energy rules were literally unrepresentable. 7. REQUIRED EXTRA: tmEvalClause now implements Granmo Eq. 6 - an all-Exclude clause outputs 1 during learning and 0 during classification. Without it, fix #1 deadlocks every clause at empty. MEASURED EFFECT (energy-threshold-turner fixture, seed 1): mean included literals per active clause 714.0 -> 13.8 active clauses 100/100 -> 53/100 nonzero corrections 8/764 -> 708/764 Tsetlin virtual hits (Linear = 27/400) 27/400 -> 69/400 Divergence achieved: offline on 7/8 fixtures, and in a live gauntlet (RandomMover: Tsetlin 199/1200 vs Linear 288/1200, vDropped=vStarved=0). Tsetlin now LEARNS but is not yet competitive with Linear - the regression head is untuned, flagged as follow-up rather than claimed as a win. Also ignores compiled test harnesses that have no file extension, which the existing '**/tests/test_*' rule misses.
79 lines
3.7 KiB
Nim
79 lines
3.7 KiB
Nim
## L1 / L3 tests for the repaired Tsetlin gun.
|
|
##
|
|
## L1 (clause sparsity + nonzero correction): before the fix the gun's Type I
|
|
## feedback never conditioned on the clause output, so every frequently-true
|
|
## literal ratcheted toward Include with no counter-force. Measured on
|
|
## `energy-threshold-turner` the clauses saturated at mean ≈714 included
|
|
## literals/clause (max 747, 100/100 active) and the correction was nonzero on
|
|
## only 8 of 764 predict calls — i.e. `Tsetlin.vHits` was effectively `Linear`.
|
|
##
|
|
## L3 (divergence): with the corrected Granmo Table 2/3 rules, the learning core
|
|
## is seeded (`makeTsetlinDriver(seed=1)`) so these numbers are reproducible.
|
|
##
|
|
## Run: nim c -r common_libs/tests/test_tsetlin_gun.nim
|
|
|
|
import std/strformat
|
|
|
|
import gun_harness/offline_range
|
|
import range_guns
|
|
import guns/tsetlin
|
|
import guns/linear
|
|
|
|
var failures = 0
|
|
proc check(name: string, ok: bool) =
|
|
if ok: echo "PASS: ", name
|
|
else: echo "FAIL: ", name; inc failures
|
|
|
|
proc main() =
|
|
# ── L1 ────────────────────────────────────────────────────────────────────
|
|
block:
|
|
let fx = synthesizeEnergyThresholdTurner()
|
|
let (drv, gun) = makeTsetlinDriver(seed = 1)
|
|
let reps = replayFixture(fx, @[drv])
|
|
let st = gun[].tmClauseStats()
|
|
echo &"L1 energy-threshold-turner (ticks={fx.states.len}):"
|
|
echo &" clauses: mean={st.meanIncluded:.1f} max={st.maxIncluded} " &
|
|
&"active={st.nActive}/{st.nClauses} meanAll={st.meanIncludedAll:.1f}"
|
|
echo &" correction nonzero on {gun[].correctionsNonzero}/{gun[].predictCalls} predict calls"
|
|
echo &" trainedShots={gun[].trainedShots} traceMisses={gun[].traceMisses} hits={reps[0].hits}/{reps[0].shots}"
|
|
check "L1: mean included literals/clause fell to a sparse regime (< 40; was ~714)", st.meanIncluded < 40.0
|
|
check "L1: at least some clauses are active (the TM is doing something)", st.nActive > 0
|
|
check "L1: the learned correction is nonzero", gun[].correctionsNonzero > 0
|
|
check "L1: correction is nonzero on most predicts", gun[].correctionsNonzero * 2 > gun[].predictCalls
|
|
check "L1: trace pairing intact (trainedShots > 0, traceMisses == 0)",
|
|
gun[].trainedShots > 0 and gun[].traceMisses == 0
|
|
echo ""
|
|
|
|
# ── L3 ────────────────────────────────────────────────────────────────────
|
|
block:
|
|
var diverged = false
|
|
var energyT, energyL: GunReport
|
|
echo "L3 divergence over synthetic range fixtures (seed=1):"
|
|
for name in SyntheticFixtureNames:
|
|
let fx = synthesizeByName(name)
|
|
let (drv, gun) = makeTsetlinDriver(seed = 1)
|
|
let reps = replayFixture(fx, @[drv, makeDriver("Linear", LinearGun())])
|
|
let t = reps[0]
|
|
let l = reps[1]
|
|
echo &" {name:<26} Tsetlin {t.hits:>4}/{t.shots:<4} Linear {l.hits:>4}/{l.shots:<4} " &
|
|
&"nzCorr={gun[].correctionsNonzero}/{gun[].predictCalls}"
|
|
if t.hits != l.hits: diverged = true
|
|
if name == "energy-threshold-turner":
|
|
energyT = t
|
|
energyL = l
|
|
echo ""
|
|
echo &"L3 energy-threshold-turner: Tsetlin {energyT.hits}/{energyT.shots} " &
|
|
&"({energyT.hitRate()*100:.1f}%) vs Linear {energyL.hits}/{energyL.shots} " &
|
|
&"({energyL.hitRate()*100:.1f}%)"
|
|
check "L3: Tsetlin.vHits diverges from Linear.vHits on at least one range fixture", diverged
|
|
check "L3: Tsetlin.vHits != Linear.vHits on energy-threshold-turner",
|
|
energyT.hits != energyL.hits
|
|
|
|
if failures > 0:
|
|
echo "\n", failures, " check(s) FAILED"
|
|
quit(1)
|
|
echo "\nAll Tsetlin-gun checks passed."
|
|
|
|
when isMainModule:
|
|
main()
|