rukh · lab

// cheatsheets · 4 módulos · 38 preguntas

Lo que te preguntarían, en corto.

Al final de cada módulo, ocho preguntas de entrevista con la respuesta que darías en un minuto. Esta página está pensada para imprimirse.

// M0

Taller: repo, entorno, datos y tablero

  1. ¿Qué es un modelo de lenguaje de ajedrez?

    Una red que predice la siguiente jugada dada la secuencia de jugadas anteriores. Se entrena con la misma pérdida que un LLM de texto (siguiente token), pero cada token es una jugada, así que las reglas y la estrategia emergen de predecir bien.

  2. ¿Por qué el ajedrez es un buen dominio para aprender IA generativa?

    Es cerrado y verificable: un motor decide si una jugada es legal y cuánto vale, así que las recompensas son gratis y objetivas. Los datos son abiertos y CC0 (Lichess), los modelos son pequeños (decenas de millones de parámetros) y la demo se entiende en cinco segundos.

  3. ¿Qué diferencia hay entre SAN, UCI, FEN y PGN?

    SAN escribe una jugada como en los libros (Nf3) y depende del tablero; UCI escribe origen y destino (g1f3) y no depende de nada; FEN describe una posición completa en una línea; PGN es el fichero de una partida entera con cabeceras y jugadas en SAN.

  4. ¿Qué papel tiene Stockfish en este curso?

    Es el juez, no el jugador. Con UCI_LimitStrength y UCI_Elo se convierte en un rival calibrado para estimar Elo; a poca profundidad da recompensas baratas para DPO y GRPO; y sus evaluaciones públicas etiquetan posiciones para el encoder.

  5. ¿Qué filtros aplica el recorte de datos y por qué?

    Ambos Elo ≥ 1800 (calidad), base de tiempo ≥ 180 s (excluye bullet, donde se juega a ciegas contra el reloj), terminación Normal o Time forfeit (excluye abandonos y trampas), ≥ 20 plies (partidas reales) y sin variantes. Se documenta todo en un manifiesto JSON.

  6. ¿Por qué la legalidad la decide el navegador y no el modelo?

    Porque el modelo solo propone una distribución sobre tokens; chess.js conoce las reglas y enmascara las jugadas ilegales antes de muestrear. Medir cuántas propone legales sin máscara es, además, una métrica de cuánto ha aprendido las reglas.

  7. ¿Para qué sirve MLflow si ya tengo los logs?

    Para que cada entrenamiento quede registrado con su configuración, sus métricas por paso y sus artefactos, comparable con los demás. Las model cards se generan desde MLflow para que ningún número de la tabla se escriba a mano.

  8. ¿Cómo se mide el progreso en cada módulo?

    Siempre con la misma tabla: tasa de jugadas legales sin máscara, exactitud top-1 y top-3 en validación, puzles resueltos por dificultad y Elo estimado contra Stockfish limitado con intervalo de confianza. Sin número no hay model card.

// M1

Del PGN al tensor: datos y tokenización

  1. ¿Qué es un token y quién decide qué cuenta como token?

    La unidad mínima que el modelo lee y escribe, convertida a un id entero por el tokenizador. Lo decide quien diseña el modelo, no los datos: es la primera hipótesis sobre el dominio ("una jugada es la unidad") y cambia la longitud de las secuencias, lo que el modelo tiene que aprender y el coste de atención, que crece con el cuadrado de la longitud.

  2. ¿Qué diferencia hay entre un vocabulario fijo, BPE y tokenización por carácter?

    El fijo se enumera sin mirar datos (en Rukh, las 1 968 jugadas UCI posibles más control y Elo: 2 030 tokens). Carácter usa un alfabeto mínimo (35 símbolos) y da secuencias cuatro o cinco veces más largas. BPE aprende fusiones de pares frecuentes desde los caracteres hasta un tamaño objetivo (4 096) y descompone lo raro en trozos, sin token desconocido.

  3. ¿Cómo se entrena un BPE en dos frases?

    Se parte de los caracteres, se cuenta el par adyacente más frecuente en el corpus, se fusiona en un token nuevo y se repite hasta el tamaño de vocabulario pedido. Tokenizar es aplicar las fusiones en el mismo orden en que se aprendieron; sobre texto UCI, las primeras fusiones reconstruyen casillas y jugadas, y las últimas, trozos de apertura.

  4. ¿Para qué sirven los tokens especiales?

    Son la interfaz de control: <bos> y <eos> marcan principio y fin, <pad> rellena y lo ignora la pérdida, <mask> es el hueco del encoder de M3, <unk> es una red de seguridad, los de resultado codifican quién ganó y los de Elo (<w1800>, <b1900>) condicionan la partida al nivel de cada jugador.

  5. ¿Por qué el Elo va al principio de la secuencia y el resultado al final?

    El modelo es causal: solo ve lo anterior. Con el Elo delante, cada jugada se predice condicionada al nivel, y en M4 basta con poner <w1500> al principio para que juegue como un 1500 sin entrenar nada nuevo. El resultado al final se predice desde las jugadas y no contamina la generación.

  6. ¿Qué es ignore_index y qué pasa si lo olvidas?

    Un parámetro de la entropía cruzada (CrossEntropyLoss(ignore_index=0)) que hace que las posiciones cuyo objetivo es <pad> no contribuyan a la pérdida ni al gradiente. Sin él, el modelo aprende que después de cualquier cosa viene <pad> y lo predice con confianza.

  7. ¿Qué es el empaquetado en flujo y qué alternativa descartamos?

    Concatenar todas las partidas codificadas en un único array uint16 y servir ventanas de 200 tokens que empiezan en un <bos>, de modo que casi no hay padding y cada ejemplo lleva los tokens de control. Descartamos las ventanas en posiciones arbitrarias (estilo nanoGPT) porque perderían el Elo del principio, y las filas de una partida con padding porque desperdician más de la mitad del cálculo.

  8. ¿Por qué partimos los datos por mes y los puzles por PuzzleId?

    Para evitar fugas: un jugador activo juega cientos de partidas con el mismo repertorio, y un split al azar pone sus partidas en los dos lados e infla la exactitud. Entrenar con enero y validar con febrero reproduce la situación real (partidas futuras). Los puzles se parten por su id con semilla 42 para que el conjunto de test sea reproducible y no cambie entre ejecuciones.

  9. ¿Qué es un memmap y por qué lo usa el dataloader?

    Un fichero mapeado a memoria: np.load(mmap_mode='r') no lee el array, deja que el sistema operativo traiga las páginas cuando se tocan. Un GB de tokens arranca al instante, varios workers comparten las mismas páginas físicas y nadie copia el fichero en RAM.

  10. ¿De dónde salen los pares de preferencia para DPO sin ejecutar Stockfish?

    Del dataset público de evaluaciones de Lichess, que tiene varias líneas principales por posición ordenadas de mejor a peor. La primera jugada de la mejor línea es la elegida y la primera jugada de una línea peor en 100 centipeones o más es la rechazada: un par por posición, equilibrado por fase y verificado legal con python-chess.

// M2

El decoder: un GPT que juega

  1. Explica la atención en tres frases.

    Cada posición proyecta su vector en una consulta (Q), una clave (K) y un valor (V). El producto escalar entre la consulta de una posición y las claves de todas las demás, dividido por la raíz de la dimensión de la cabeza y pasado por softmax, da un reparto de pesos que suma 1. La salida es la media de los valores ponderada por esos pesos: softmax(QKᵀ/√d)·V. La escala 1/√d existe porque el producto escalar de d dimensiones tiene varianza d y sin ella el softmax saturaría y el gradiente se anularía.

  2. ¿Qué es la máscara causal y qué pasa si falta?

    Poner a menos infinito, antes del softmax, el peso de toda clave posterior a la consulta, de modo que la matriz de atención queda triangular inferior. Sin ella la posición t puede leer el token t+1, que es justo el que tiene que predecir: la tarea pasa de predecir a copiar, la pérdida se desploma y el top-1 se dispara, pero al generar (donde no hay futuro) el modelo juega al azar. Se detecta porque la legalidad sin máscara se hunde, y se verifica con un test: cambiar un token futuro no debe mover los logits pasados ni un decimal.

  3. ¿Por qué pre-norm y no post-norm?

    Pre-norm pone el LayerNorm dentro de la rama, antes de la subcapa (x = x + attn(ln(x))), así que la conexión residual queda libre: la derivada de cada bloque es la identidad más la de la subcapa y el gradiente llega íntegro desde la pérdida hasta el embedding. Post-norm normaliza después de la suma, mete la normalización en el camino residual y hace que una pila profunda solo entrene con warmup largo e inicialización muy cuidada. Es un cambio de dos caracteres que convierte doce capas entrenables con suerte en doce capas entrenables.

  4. ¿Qué hace el warmup y qué pasa sin él?

    Sube la tasa de aprendizaje linealmente desde cero durante los primeros pasos (1 000 en rukh-small) antes de empezar el coseno. En los primeros pasos, Adam estima el segundo momento con un puñado de gradientes y su paso normalizado puede mover un parámetro tanto como la tasa entera; sobre una red recién inicializada eso satura la atención y el modelo cae en predecir siempre las jugadas más frecuentes. No suele explotar con un NaN: se queda en una pérdida mediocre que ya no baja, que es peor porque no se nota.

  5. Temperatura y top-k: ¿en qué se diferencian?

    La temperatura divide los logits antes del softmax y reescala toda la distribución: por debajo de 1 la concentra (con 0 es el argmax), por encima la aplana. El top-k no reescala, recorta: deja solo los k tokens de mayor probabilidad y pone el resto a cero. Son complementarios, no alternativas. Con 2 030 jugadas, la cola de mil opciones improbables suma un porcentaje apreciable de disparates; el top-k la elimina y la temperatura decide cuánto arriesga el modelo dentro de lo que queda. Rukh usa temperatura 0,6 y k = 20.

  6. ¿Qué mide la legalidad sin máscara, por qué hay dos cifras y por qué no vale medirla con máscara?

    Mide el porcentaje de jugadas legales que propone el modelo cuando se le deja emitir el token que quiera, sobre 10 000 posiciones de validación. Se publica dos veces: legality_argmax toma el token más probable (sin temperatura ni top-k) y legality_sampled saca uno como lo saca la demo (temperatura 0,6 y top-k 20), que es siempre la menor de las dos porque muestrear mete cola. El listón de ≥ 99 % para rukh-small es el argmax, porque habla de lo que saben los pesos y no de un ajuste del muestreador; la muestreada es lo que experimenta un jugador y va al lado con su definición. Con la máscara de legalidad puesta el 100 % está garantizado por construcción, así que estarías midiendo a python-chess y no al modelo.

  7. ¿Por qué bf16 y no fp16?

    Los dos ocupan 16 bits, pero reparten distinto. fp16 tiene 5 bits de exponente y 10 de mantisa: mucha precisión y poco rango, con el mínimo normal en torno a 6e-5, justo donde viven los gradientes de una red profunda, así que se van a cero y hace falta un GradScaler con escalado dinámico de la pérdida. bf16 tiene 8 bits de exponente (el mismo rango que fp32) y 7 de mantisa: menos precisión por número y nada que se desborde por abajo, así que entrena sin escalar nada. En una GPU moderna es nativo, así que la simplicidad sale gratis.

  8. ¿Qué es un checkpoint y qué tiene que llevar dentro?

    Una fotografía del entrenamiento en disco. No basta con los pesos: lleva el paso alcanzado, el estado del optimizador (los momentos de Adam), el estado de los generadores aleatorios, la configuración del modelo y de la tirada y la procedencia (hash del vocabulario, hash del manifiesto de datos y SHA de git). Con eso se puede reanudar exactamente donde se cortó, evaluar el modelo meses después y decir con qué datos exactos se entrenó. Rukh escribe uno cada 1 000 pasos más best.pt por pérdida de validación.

  9. ¿Qué es la perplejidad?

    La exponencial de la entropía cruzada media. Se lee como entre cuántas opciones equiprobables duda el modelo: con un vocabulario de 2 030 jugadas, un modelo sin entrenar tiene perplejidad 2 030 y pérdida ln(2030) ≈ 7,6; una pérdida de 3,0 es perplejidad 20, es decir, acierta como si eligiera al azar entre veinte jugadas. Como en una posición típica hay unas treinta legales, esa cifra ya implica bastante aprendizaje. No es una métrica aparte: es la misma pérdida en otra escala.

  10. ¿Qué significa un intervalo de confianza del Elo y por qué importa?

    El Elo estimado es el valor que mejor explica los resultados contra rivales de fuerza conocida según la fórmula logística P = 1/(1+10^((rival−modelo)/400)); el intervalo del 95 % dice cuánto podría moverse ese valor si se repitieran las partidas, y se calcula por bootstrap remuestreando los resultados y reajustando mil veces. Con 100 partidas al 50 % la desviación típica ronda los 35 puntos; con las 800 de la suite completa, unos 12. Es decir: una mejora de 10 o 20 puntos de Elo medida con cien partidas no es una mejora, es ruido, y presumir de ella es el error clásico de los benchmarks.

// M3

El encoder: entender la posición

  1. ¿En qué se diferencia un encoder bidireccional de un decoder causal?

    En una línea: la máscara. El decoder pasa `is_causal=True` a la atención y cada posición solo ve hacia atrás, que es lo que hace posible generar de izquierda a derecha sin hacer trampa; el encoder pasa `causal=False` y cada posición ve toda la secuencia, incluidas las jugadas posteriores. Todo lo demás —embeddings, multi-cabeza, bloques pre-norm, LayerNorm final— es el mismo código: en Rukh, `PositionEncoder` y `MoveDecoder` construyen sus bloques desde el mismo `rukh.models.layers`. Lo que se gana es un contexto completo para representar; lo que se pierde es la capacidad de generar, porque la tarea de predecir el siguiente token con el siguiente token a la vista es trivial.

  2. ¿Qué es el MLM y por qué 80/10/10?

    Masked language modeling: se esconde el 15 % de los tokens y el modelo los reconstruye desde los dos lados. De los tokens elegidos, el 80 % se sustituye por `<mask>`, el 10 % por otro token al azar y el 10 % se deja como está. Si todos fueran `<mask>`, el modelo solo vería ese símbolo durante el preentrenamiento y nunca durante el afinado, así que aprendería una representación que únicamente funciona cuando falta algo. El 10 % aleatorio le obliga a desconfiar de lo que lee (un token que *está* puede ser falso) y el 10 % intacto le obliga a representar todas las posiciones, no solo las marcadas. En Rukh se aplica a jugadas y los tokens de control (`<bos>`, Elo, resultado, `<eos>`) nunca se tapan: son la condición, no la señal.

  3. ¿Qué es el pooling y cuándo usar CLS o media?

    Reducir los T vectores que devuelve el encoder a uno solo que represente la secuencia. `cls` toma la posición 0, un token que no aporta contenido y cuya única función es acumular el resumen que la atención le traiga; `mean` promedia los vectores de los tokens reales. La media es más robusta y es el defecto de Rukh, pero tiene una condición: hay que ignorar el relleno, porque promediar los vectores de los `<pad>` diluye la representación en una cantidad que depende de cuánto se rellenó ese lote, es decir, el vector de una misma posición cambiaría según con quién le toque viajar. `PositionEncoder.pool` lee la máscara de `idx` cuando no se le pasa una.

  4. ¿Qué diferencia hay entre un probe lineal y un fine-tuning completo, y qué mide cada uno?

    El probe congela el encoder y entrena solo una capa lineal encima del vector agrupado: mide si la información ya estaba en la representación, porque una capa lineal no puede aprenderla por su cuenta. El fine-tuning completo mueve todos los pesos: mide lo mejor que puede hacer esa arquitectura con esas etiquetas, y suele ganar, pero necesita más etiquetas, se sobreajusta antes y ya no dice nada sobre el preentrenamiento. Entre los dos está `last-n`, que descongela las últimas capas y la norma final. Y el hito midió lo contrario de lo esperable: `last-n` sacó el mejor error de valor (0,1180) por delante de `full` (0,1354) y del probe (0,1429), porque descongelarlo todo con 438 093 etiquetas aleja las capas de abajo de la representación que dejó el preentrenamiento; `full` solo gana en la exactitud del resultado. Es una tirada por modo, así que la ordenación es sugerente y harían falta varias semillas para establecerla. En Rukh son los tres modos de `rukh train heads --mode probe|last-n|full`, y un test comprueba que en `probe` los pesos del encoder salen idénticos bit a bit.

  5. ¿Qué mide el F1, cuándo no sirve la exactitud y dónde se elige el umbral?

    F1 es la media armónica de precisión (de lo que marqué, cuánto era de verdad) y exhaustividad (de lo que había, cuánto marqué). La exactitud deja de servir en cuanto una clase es rara, y en M3 lo es: los errores son el **3,72 %** de las filas, así que un detector que conteste siempre «no hay error» saca **96,3 %** sin mirar el tablero y la cabeza real saca 96,78 %. Peor todavía, el F1 en un umbral arbitrario tampoco mide la representación: las probabilidades de esta cabeza van de 0,0040 a 0,2568, así que en el umbral de fábrica de 0,5 no se dispara nunca y su F1 es **exactamente 0,0000**. El mismo modelo tiene **0,738 de ROC AUC**. El procedimiento honesto es partir las filas etiquetadas en dos por `game_id`, elegir el umbral en la mitad `tune` (aquí 0,128) y publicar el F1 medido en la mitad `score` (aquí 0,179), que no ha visto ningún umbral. Elegir el umbral en las filas que luego se puntúan convierte la estimación en un récord personal.

  6. ¿Qué es una fuga de datos y por qué aquí el split va por partida?

    Cualquier camino por el que información de validación llega al entrenamiento. Nunca falla con un error: los números salen mejores y el problema se descubre en producción. En M3 el peligro concreto es partir por posición: dos posiciones consecutivas de la misma partida se diferencian en una jugada, así que la del ply 30 en entrenamiento y la del 31 en validación es casi la misma posición y un modelo que memoriza puntúa como uno que entiende. Rukh parte por `game_id` con una función pura de CRC-32 sobre el id y la semilla, y un test comprueba que ningún `game_id` está en los dos lados.

  7. ¿Por qué se correlaciona el valor con el `cp` de Stockfish y por qué dos correlaciones?

    Porque `cp` es la única referencia objetiva de «cuánto vale esta posición» que hay a escala, y porque una regresión necesita una medida de calidad que no dependa de un umbral. Se correlaciona contra `tanh(cp / 400)`, la escala acotada con la que se entrena la cabeza, y nunca contra el `cp` en bruto: un mate vale ±9 999 centipeones y media docena de filas así decidirían el Pearson del conjunto entero. Se publican las dos correlaciones: Pearson mide si los valores se alinean en una recta, Spearman solo si el orden coincide. Discrepan exactamente cuando el modelo tiene bien el orden y mal la escala, que es lo que le hace un `tanh` acotado, así que dar solo una oculta la mitad del diagnóstico. `GOAL.md` pide ≥ 0,80 sobre Spearman y **el hito se queda en 0,407 con la entrada por jugadas y 0,422 con la de casillas: criterio no cumplido**, publicado sin cumplir y con el diagnóstico al lado.

  8. ¿Qué es una línea base y por qué la de M3 es deliberadamente tonta?

    El rival contra el que se mide el modelo para saber si su número significa algo. La de M3 cuenta material (peón 1, caballo y alfil 3, torre 5, dama 9), le suma movilidad y llama error a toda jugada que pierda un punto neto tras la captura más rentable del rival a un ply. Es tonta a propósito: una línea base que ya entendiera los sacrificios no sería un suelo, sería un competidor, y el margen dejaría de decir «el modelo aprendió algo más que contar piezas». Lo esencial es medirla exactamente igual que al modelo: mismas filas, mismas etiquetas, misma definición de acierto. Medido: la heurística saca 8,9 % de F1 y el encoder 17,9 %, **+9,0 puntos**, por encima de los cinco que pide `GOAL.md`. Y la comparación no es simétrica en dos sentidos opuestos que el informe dice en voz alta: la heurística ve la posición anterior y la jugada, que el encoder no ve; y la heurística es una regla sin umbral que ajustar, mientras que el modelo se mide en su mejor punto de operación.

  9. ¿Qué aporta la curva por número de etiquetas?

    Dice cuántas etiquetas hacen falta de verdad. Se entrena la misma receta con el 10, 25, 50 y 100 % de las etiquetas de entrenamiento y se dibuja la métrica contra el número de filas. Si la curva se aplana pronto, etiquetar más es tirar dinero y el cuello de botella está en el modelo o en la tarea; si sigue subiendo al 100 %, etiquetar más es la inversión más rentable que queda. Los subconjuntos son **anidados** (el 10 % está dentro del 25 %, y este dentro del 50 %) porque con muestras independientes un bache a mitad de la curva podría ser mala suerte del sorteo y estaríamos midiendo ruido de muestreo en vez del valor de los datos. Medido en M3: plana desde el 25 % (0,1217 / 0,1192 / 0,1210 / 0,1215 de error de valor al 10, 25, 50 y 100 %), así que más allá de unas cien mil etiquetas más evaluaciones de Stockfish no compran nada. Y la dispersión entre esos cuatro puntos es la misma que entre dos tiradas idénticas, que es la segunda lección: antes de explicar una diferencia pequeña, mide cuánto se mueve tu montaje cuando no cambias nada.

  10. ¿En qué se diferencia este modelo del de M2?

    En la máscara, en la tarea y en para qué sirve. El de M2 es causal, se entrena a predecir la siguiente jugada y genera: es lo que juega en la demo. El de M3 es bidireccional, se entrena tapando jugadas en medio de la partida y no genera nada: produce un vector por posición del que salen el valor, la detección de errores y el resultado. También es más pequeño (8 capas, d=384, 15 052 800 parámetros frente a 12 capas, d=512 y 38 971 392) y usa `ignore_index=-100` en vez del `0` del decoder, porque en la entrada por casillas el `0` es un token que sí se puede pedir que prediga. En la demo conviven: el decoder elige jugada, el encoder pinta la barra, y cada uno en su propio worker.