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
- FiveWin for Harbour instalado (este árbol en
C:\fwteam). - Una variante de compilador ya capaz de construir samples (p.ej. Harbour + BCC32 →
hb32). Ver Sistema de compilación. - Librerías FWH reconstruidas con
tensorlogic.prgyfwtensor.c:
(Usacd /d C:\fwteam C:\fwteam\mfwh_new.bat hb32hm32,hm64,hg64, … para otras variantes.) - Opcional pero recomendado: OpenBLAS. Coloca
libopenblas.dllen el PATH o junto al exe (hay una copia ensamples\ai\libopenblas.dll). Sin él los bucles C puros funcionan, solo más lentos. Fuerza el modo portable conset FWT_BLAS_DLL=off.
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)
- Un tensor es un array n-D de float32 en memoria C.
- Un einsum multiplica en índices compartidos (join) y suma los índices ausentes de la salida (proyección).
- Un programa de tensor logic son tensores con nombre + strings de ecuación
evaluados con
Run()(punto fijo) oForward()(una pasada para entrenar). - 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
| Orden | Sample | Qué aprendes |
|---|---|---|
| 1 | tltutor.prg | Las nueve lecciones de esta página. |
| 2 | tensorlogic.prg | Tests del motor, entrenamiento XOR (9 PASS). |
| 3 | tldatalog.prg | Reglas sobre un DBF familiar + temperatura en embeddings. |
| 4 | tltransformer.prg | Modelo estilo GPT como ecuaciones; paridad con Transformer clásico. |
| 5 | tlbench.prg | Números de velocidad BLAS vs C. |
| 6 | tlminillm.prg | MLA, RoPE, MoE, predicción multi-token — siguen siendo ecuaciones. |
| 7 | tlfwcode.prg | Entrena un LM char-level con fuentes FWH reales (warmup, val, checkpoints). |
| 8 | tlc.prg | Entrenamiento 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:
- JOIN: un índice compartido por dos operandos multiplica sus elementos coincidentes.
- PROYECCIÓN: un índice ausente de la salida se suma (desaparece).
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.
- Un transformer real:
samples/ai/tltransformer.prgescribe ~15 ecuaciones por capa y reproduce de cerca la salida de la claseTransformerclásica con los mismos pesos (test:tl_parity_forwardseq). - Un LLM moderno:
samples/ai/tlminillm.prgañade las técnicas de DeepSeek/MiMo — atención latente MLA, RoPE (¡un join con matriz de permutación con signo!), mixture-of-experts, predicción multi-token — todo siguen siendo ecuaciones. - Entrenar con datos reales:
samples/ai/tlfwcode.prgentrena un modelo char-level con el código fuente de FWH: warmup+coseno, split train/val, checkpoints del mejor-val y resume (set TLFW_RESUME=1). Ajusta conTLFW_STEPS,TLFW_BATCH,TLFW_LAYERS,TLFW_DMODEL,TLFW_HEADS,TLFW_WINDOW,TLFW_LR, … - Entrenamiento sintético de código:
samples/ai/tlc.prg— generador + oráculo, aprendizaje de ejecución, decoding con gramática (100% programas válidos incluso sin entrenar). - Velocidad:
samples/ai/tlbench.prg— con OpenBLAS el einsum alcanza 497 GFLOPS en un matmul de 1024 (PyTorch/MKL: 982 en la misma máquina).
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
| Objetivo | API |
|---|---|
| Ligar datos | oTL:Let( "X", aOrTensor ) |
| Peso entrenable | oTL:Param( "W", aOrTensor ) |
| Añadir ecuación | oTL:Eq( "Y[i] = relu( W[i,j] * X[j] )" ) |
| Punto fijo simbólico | oTL:Run( 20 ) |
| Paso de entrenamiento | Forward → ZeroGrad → Seed → Backward → ClipGrads → StepAdam |
| Leer resultado | oTL:Get( "Y" ) / GetArray( "Y" ) |
| DBF → relación | oDom:Harvest y luego LetDbf |
| Relación → nombres | oTL:Query( "R", oDom ) |
| Checkpoint | SaveParams / LoadParams |
| Transformer listo | TTransformerTL():New(...) |
| ¿BLAS activo? | FWT_BlasActive() |