Tensor Logic: de 0 a 100

Compañero ejecutable: samples/ai/tltutor.prg — compílalo, ejecútalo y lee tltutor.log junto a esta página

Este tutorial te lleva de "qué es un tensor" a entrenar un modelo de lenguaje con el código fuente de FWH, usando una sola idea del Tensor Logic de Pedro Domingos (arXiv:2510.12269): una regla lógica y una suma de Einstein son la misma operación. Cada lección es una función de tltutor.prg — las salidas mostradas son su salida real.

API completa: referencia Tensor Logic.

Cómo usar Tensor Logic — paso a paso

Paso 0 — Requisitos

Paso 1 — Compilar y ejecutar el sample del tutorial

cd /d C:\fwteam\samples\ai
C:\fwteam\samples\build_new.bat tltutor hb32
tltutor.exe
type tltutor.log

Debes ver nueve bloques de lección que terminan con TUTORIAL COMPLETE — next: tltransformer.prg, tlminillm.prg, tlfwcode.prg. Mantén tltutor.log abierto mientras lees las lecciones.

Paso 2 — Modelo mental (30 segundos)

flowchart TD T[FW_Tensor float32] --> E[FWT_EinSum join + proyectar] E --> P[Programa TTensorLogic: tensores con nombre + Eq] P --> N[Redes: atención, MLP, transformer] P --> S[Simbólico: Datalog sobre DBF] P --> A[Autodiff: Forward / Seed / Backward / StepAdam]
  1. Un tensor es un array n-D de float32 en memoria C.
  2. Un einsum multiplica en índices compartidos (join) y suma los índices ausentes de la salida (proyección).
  3. Un programa de tensor logic son tensores con nombre + strings de ecuación evaluados con Run() (punto fijo) o Forward() (una pasada para entrenar).
  4. El gradiente de una ecuación es el mismo einsum con un factor y la salida intercambiados — cualquier programa es entrenable sin backprop a mano.

Paso 3 — Tu primer programa (copiar y pegar)

#include "FiveWin.ch"

FUNCTION Main()
   LOCAL oTL := TTensorLogic():New()

   oTL:Let( "W", { { 1, -1, 0 }, { 0, 1, -1 } } )
   oTL:Let( "X", { 1, 2, 0.5 } )
   oTL:Let( "B", { 2, -2 } )
   oTL:Eq( "Y[i] = step( W[i,j] * X[j] + B[i] )" )
   if ! oTL:Run()
      ? "Error:", oTL:cError
      RETURN NIL
   endif
   ? oTL:GetArray( "Y" )     // {1, 0}
   ? "BLAS activo?", FWT_BlasActive()
RETURN NIL
C:\fwteam\samples\build_new.bat myfirst hb32
myfirst.exe

Paso 4 — Entrenar algo (puerta AND)

oTL := TTensorLogic():New()
oTL:Let(   "X", { { 0, 0 }, { 0, 1 }, { 1, 0 }, { 1, 1 } } )
oTL:Let(   "Ones", { 1, 1, 1, 1 } )
oTL:Param( "W", { { 0.1 }, { -0.1 } } )   // Param = entrenable
oTL:Param( "B", { 0 } )
oTL:Eq( "Y[n,o] = sigmoid( X[n,f] * W[f,o] + Ones[n] * B[o] )" )
tT := FWT_FromArray( { { 0 }, { 0 }, { 0 }, { 1 } } )

for e := 1 to 300
   oTL:Forward()
   oTL:ZeroGrad()
   oTL:Seed( "Y", FWT_Scale( FWT_Sub( oTL:Get( "Y" ), tT ), 2 ) )
   oTL:Backward()
   oTL:ClipGrads( 1.0 )
   oTL:StepAdam( 0.05 )
next
// Y ≈ {0.00, 0.11, 0.11, 0.88} para targets {0,0,0,1}
oTL:SaveParams( "and.chk" )

Ese bucle — Forward / ZeroGrad / Seed / Backward / Clip / Adam — es el mismo que usan los samples de modelos de lenguaje.

Paso 5 — Sube la escalera de samples

OrdenSampleQué aprendes
1tltutor.prgLas nueve lecciones de esta página.
2tensorlogic.prgTests del motor, entrenamiento XOR (9 PASS).
3tldatalog.prgReglas sobre un DBF familiar + temperatura en embeddings.
4tltransformer.prgModelo estilo GPT como ecuaciones; paridad con Transformer clásico.
5tlbench.prgNúmeros de velocidad BLAS vs C.
6tlminillm.prgMLA, RoPE, MoE, predicción multi-token — siguen siendo ecuaciones.
7tlfwcode.prgEntrena un LM char-level con fuentes FWH reales (warmup, val, checkpoints).
8tlc.prgEntrenamiento sintético de código + decoding con gramática.
C:\fwteam\samples\build_new.bat tensorlogic hb32
tensorlogic.exe
type tensorlogic.log

Lección 1 — Tensores

Un tensor es un array n-dimensional de números. En FWH es un FW_Tensor: un buffer float32 plano en memoria C (rápido, compacto), creado e inspeccionado desde PRG:

t := FWT_New( { 2, 3 } )                 // 2x3, a ceros
FWT_Set( t, 1, 10 )                      // indexado plano 1-based, row-major
FWT_Set( t, 6, 60 )
? FWT_Shape( t )                         // {2, 3}
t := FWT_FromArray( { { 1, 2 }, { 3, 4 } } )   // desde array anidado

Salida esperada (de tltutor.log):

=== Lesson 1: tensors ===
shape = {2, 3}, size = 6
t = {{10.00, 0.00, 0.00}, {0.00, 0.00, 60.00}}
from array = {{1.00, 2.00}, {3.00, 4.00}}

Lección 2 — Einsum: la única operación que necesitas

Una especificación einsum nombra los índices de cada operando y los de la salida. Dos reglas:

A := FWT_FromArray( { { 1, 2, 3 }, { 4, 5, 6 } } )
FWT_EinSum( "ij->i",     A )        // sumas por fila  -> {6, 15}
FWT_EinSum( "ij->ji",    A )        // transpuesta
FWT_EinSum( "ij,jk->ik", A, B )     // producto matricial (join j, proyecta j)
FWT_EinSum( "i,j->ij",   v, w )     // producto exterior  (sin proyección)
FWT_EinSum( "i,i->",     v, v )     // producto escalar   (todo proyectado)

Toda el álgebra lineal es una primitiva con distintos patrones de índices. Con OpenBLAS presente corren sobre cblas_sgemm (ver la referencia para el benchmark de 497 GFLOPS).

=== Lesson 2: einsum ===
row sums     'ij->i'    = {6.00, 15.00}
transpose    'ij->ji'   = {{1.00, 4.00}, {2.00, 5.00}, {3.00, 6.00}}
matmul       'ij,jk->ik' = {{14.00, 32.00}, {32.00, 77.00}}
dot 'i,i->' of {1,0,2} = 5.00

Lección 3 — Tu primer programa: ecuaciones

TTensorLogic guarda tensores con nombre y ecuaciones escritas como strings. Este perceptrón completo es UNA ecuación:

oTL := TTensorLogic():New()
oTL:Let( "W", { { 1, -1, 0 }, { 0, 1, -1 } } )
oTL:Let( "X", { 1, 2, 0.5 } )
oTL:Let( "B", { 2, -2 } )
oTL:Eq( "Y[i] = step( W[i,j] * X[j] + B[i] )" )
oTL:Run()
? oTL:GetArray( "Y" )        // {1, 0}

Léelo como einsum: j se joinea y proyecta (eso es W·X), + B[i] añade un segundo término, step() es la no-linealidad. Disponibles: step, sigmoid, relu, tanh, gelu, softmax, lnorm.

=== Lesson 3: first equation (a perceptron) ===
W.X + B = {1, -0.5} ; step -> Y = {1.00, 0.00}

Lección 4 — Las reglas lógicas SON ecuaciones

Guarda una relación como tensor booleano (1 = el hecho es cierto). Entonces la regla Datalog Grandparent(x,z) <- Parent(x,y), Parent(y,z) es literalmente el mismo patrón de ecuación: la variable compartida y es el join, y step() convierte "al menos un camino" en verdadero:

oTL:Let( "Parent", { {0,1,0,0}, {0,0,1,0}, {0,0,0,1}, {0,0,0,0} } )   // 1->2->3->4
oTL:Eq( "Grandparent[x,z] = step( Parent[x,y] * Parent[y,z] )" )
oTL:Run()      // Grandparent: (1,3) y (2,4)

Esta es la tesis central del paper, en código funcionando: redes neuronales (lección 3) y lógica (lección 4) corren en el mismo motor.

=== Lesson 4: logic rules (Grandparent) ===
Grandparent = {{0,0,1,0}, {0,0,0,1}, {0,0,0,0}, {0,0,0,0}}

Lección 5 — Recursión: forward chaining hasta punto fijo

Run() reevalúa todas las ecuaciones hasta que nada cambia, así que las reglas recursivas simplemente funcionan. Clausura transitiva:

oTL:Let( "Ancestor", { {0,0,0,0}, {0,0,0,0}, {0,0,0,0}, {0,0,0,0} } )  // semilla vacía
oTL:Eq( "Ancestor[x,y] = step( Parent[x,y] + Ancestor[x,z] * Parent[z,y] )" )
oTL:Run( 20 )  // Ancestor de 1: {2,3,4}; de 2: {3,4}; de 3: {4}
=== Lesson 5: recursion to fixpoint (Ancestor) ===
Ancestor = {{0,1,1,1}, {0,0,1,1}, {0,0,0,1}, {0,0,0,0}}

Lección 6 — Datalog sobre tus tablas DBF

Una tabla xBase ES una relación. TTLDomain mapea nombres de entidades a índices, LetDbf() convierte los registros, Query() devuelve los resultados como nombres:

oDom := TTLDomain():New()
oDom:Harvest( "FAM", { "PARENT", "CHILD" } )       // registra todos los nombres PRIMERO
oTL:LetDbf( "Parent", "FAM", { "PARENT", "CHILD" }, oDom )
oTL:Eq( "Grandparent[x,z] = step( Parent[x,y] * Parent[y,z] )" )
oTL:Run()
AEval( oTL:Query( "Grandparent", oDom ), ;
       {| a | QOut( a[1] + " -> " + a[2] ) } )     // abe -> bart, abe -> lisa
=== Lesson 6: Datalog over a DBF ===
grandparent: abe -> bart
grandparent: abe -> lisa

Consejo: haz siempre Harvest() de cada relación antes de dimensionar tensores, para que el dominio esté completo y las dimensiones coincidan entre reglas.

Lección 7 — Bloques neuronales: softmax y atención

Un . tras un índice del lado izquierdo marca el eje del softmax. Una cabeza de atención son seis ecuaciones — compara con los cientos de líneas de una implementación manual:

oTL:Eq( "Q[p,k] = X[p,d] * WQ[d,k]" )
oTL:Eq( "K[p,k] = X[p,d] * WK[d,k]" )
oTL:Eq( "V[p,k] = X[p,d] * WV[d,k]" )
oTL:Eq( "S[p,q] = Q[p,k] * K[q,k] * 0.7071" )   // scores escalados (1/sqrt(dk))
oTL:Eq( "A[p,q.] = softmax( S[p,q] )" )          // las filas suman 1
oTL:Eq( "O[p,k] = A[p,q] * V[q,k]" )             // valores atendidos
=== Lesson 7: attention in 6 equations ===
attention row 1 = {0.33, 0.33, 0.34}
row sum = 1.000000  (softmax => 1)

Lección 8 — Autodiff: el gradiente es otro programa

Para Z = X1 * X2 * ... * Xn, el gradiente respecto a cualquier factor es el mismo einsum con ese factor y la salida intercambiados. El motor aplica esta regla mecánicamente a cada ecuación: ninguna capa necesita backprop escrito a mano:

oTL:Param( "W", { { 0.3, -0.2 }, { 0.1, 0.4 } } )   // Param = entrenable
oTL:Eq( "Y[i] = sigmoid( W[i,j] * X[j] )" )
oTL:Forward()                                        // cachea pre-activaciones
oTL:ZeroGrad()
oTL:Seed( "Y", FWT_Scale( FWT_Sub( oTL:Get("Y"), tTarget ), 2 ) )  // dL/dY
oTL:Backward()                                       // barrido inverso
? FWT_Get( oTL:GetGrad( "W" ), 1 )   // -0.19283922
// diferencias finitas sobre el mismo peso: -0.19283742  — coinciden
=== Lesson 8: automatic differentiation ===
dL/dW[1,1] analytic = -0.19283922, finite differences = -0.19283742

Usa Forward() (no Run()) al entrenar: cachea las pre-activaciones que Backward() necesita.

Lección 9 — Entrenar de verdad: Adam, clipping, batches

El bucle profesional añade optimizador Adam (kernel C), clipping global de gradientes, acumulación por mini-batch y checkpointing. Aprendiendo la puerta AND:

for e := 1 to 300
   oTL:Forward()
   oTL:ZeroGrad()
   oTL:Seed( "Y", FWT_Scale( FWT_Sub( oTL:Get("Y"), tT ), 2 ) )
   oTL:Backward()
   oTL:ClipGrads( 1.0 )
   oTL:StepAdam( 0.05 )
next        // Y -> {0.00, 0.11, 0.11, 0.88} para targets {0,0,0,1}
=== Lesson 9: Adam training (AND gate) ===
epoch  100  loss 0.179205
epoch  200  loss 0.073157
epoch  300  loss 0.039337
Y after training (targets 0,0,0,1) = {{0.00}, {0.11}, {0.11}, {0.88}}

Para batches con TTransformerTL:

oT:oTL:ZeroGrad()
for i := 1 to nBatch
   oT:AccumStep( aWin[i], aTgt[i], 1 / nBatch )  // grads de params se acumulan
next
oT:oTL:ClipGrads( 1.0 )
oT:oTL:StepAdam( nLr )
oT:oTL:SaveParams( "model.chk", { "vocab" => aVocab } )

Lección 10 — Fachada transformer y más allá

No hace falta escribir todas las ecuaciones a mano. TTransformerTL construye un stack estilo GPT por ti:

oT := TTransformerTL():New( 2, 8, 2, nVocab )   // capas, d_model, cabezas, vocab
aProbs := oT:ForwardSeq( { 2, 3, 4, 5, 6 } )
nLoss  := oT:TrainStep( aIds, aTargets, 0.01 )
aGen   := oT:Generate( { 1 }, 8 )

Por debajo sigue siendo un programa TTensorLogic — inspecciona oT:oTL:aEqs o abre samples/ai/tltransformer.prg.

El modelo mental que debes quedarte: todo — una regla, una capa, un gradiente — es un conjunto de ecuaciones tensoriales, y una ecuación es un einsum más una no-linealidad.

Chuleta

ObjetivoAPI
Ligar datosoTL:Let( "X", aOrTensor )
Peso entrenableoTL:Param( "W", aOrTensor )
Añadir ecuaciónoTL:Eq( "Y[i] = relu( W[i,j] * X[j] )" )
Punto fijo simbólicooTL:Run( 20 )
Paso de entrenamientoForward → ZeroGrad → Seed → Backward → ClipGrads → StepAdam
Leer resultadooTL:Get( "Y" ) / GetArray( "Y" )
DBF → relaciónoDom:Harvest y luego LetDbf
Relación → nombresoTL:Query( "R", oDom )
CheckpointSaveParams / LoadParams
Transformer listoTTransformerTL():New(...)
¿BLAS activo?FWT_BlasActive()

Ver También