English · Español
Lab 01 — Implementar BPE en Python puro¶
Objetivo: escribir
src/minitoken/bpe.pydesde cero. Train + encode + decode + save/load + tests de propiedades de ida y vuelta.Tiempo estimado: 4–6 horas, posiblemente a lo largo de varias sesiones.
Prerrequisito: lab 00 comprometido;
src/minitoken/BLUEPRINT.mdrevisado y refinado según A12.
Lo que produces¶
src/minitoken/__init__.py— re-exportaBPETokenizer,Vocab.src/minitoken/bpe.py— el trainer + encoder + decoder.src/minitoken/vocab.py— dataclassVocab+ helpers de serialización.tests/test_bpe.py— tests unitarios + de propiedades.tests/test_bpe.md(o un docstring al inicio del fichero) — la lista de tests escrita antes de los tests, según CLAUDE.md §1.
Los cuatro ficheros deben pasar mypy --strict y el linting de ruff.
Lista de tests (escribe esto PRIMERO, según CLAUDE.md §1)¶
Antes de cualquier implementación, escribe la lista de tests que pretendes escribir — como comentarios o en un fichero markdown. La lista debe incluir como mínimo:
-
test_train_minimal_corpus— entrena sobre el corpus juguete del lab-00, verifica que los primeros 5 merges casan con la traza hecha a mano. -
test_encode_decode_roundtrip_ascii— para 20 cadenas ASCII elegidas a mano,decode(encode(s)) == s. -
test_encode_decode_roundtrip_unicode— UTF-8 multibyte cubriendo acentos españoles ("mañana","¿cómo estás?") y emojis ("🦊"). -
test_encode_decode_property(hypothesis) — 1000 cadenas utf-8 aleatorias (ASCII + rango español + emoji), ida y vuelta. -
test_determinism— entrena dos veces con la misma semilla → vocab + merges idénticos (byte-iguales). -
test_tiebreak— fixture de corpus con un empate conocido en conteo de pares; verifica que se aplica el desempate lex-ascendente. -
test_save_load_roundtrip—BPETokenizer.load(t.save(path)).vocab == t.vocabbyte-idéntico. -
test_empty_string—encode("") == [],decode([]) == "". -
test_single_byte—encode("a")devuelve una lista de longitud 1. -
test_special_tokens_atomic— los tokens especiales nunca se fusionan a través. -
test_vocab_size_target_reached— entrenar convocab_size=300produce exactamente 300 entradas (256 bytes + especiales + merges). -
test_out_of_bounds_id—decode([99999])lanza un error claro. -
test_mypy_strict— implícito (el paso de CI lo captura).
TODOs¶
Bloque A — src/minitoken/vocab.py¶
- Define
Vocabcomo un dataclass congelado conid_to_bytes: tuple[bytes, ...],bytes_to_id: dict[bytes, int],id_to_display: tuple[str, ...]. - Añade
Vocab.save(path)→ escribevocab.json(id ↔ display ↔ hex(bytes) para inspección humana). - Añade
Vocab.load(path)→ al revés. - Tests unitarios: save → load → igualdad.
Bloque B — src/minitoken/bpe.py: entrenamiento¶
-
class BPETokenizercon__init__(self). -
_reserve_specials_and_bytes(special_tokens)— rellena los IDs 0..|S|-1 + 0..255 de bytes base. -
_corpus_to_pretokens(corpus)— convierte cada cadena a una tupla debytes-singletons; deduplica a un Counter. -
_count_pairs(pretokens)— devuelvedict[(bytes, bytes), int]. -
_pick_winner(pair_counts)— implementamin(..., key=lambda p: (-count, p))(conteo máximo, luego par lexicográficamente menor). Documenta el desempate. -
_apply_merge(pretokens, winner)— devuelve nuevo dict de pretokens con el par reemplazado. -
train(corpus, vocab_size, special_tokens, verbose)— el bucle principal. - Registra una barra de progreso si
verbose=True.
Bloque C — src/minitoken/bpe.py: encode/decode¶
-
encode(text: str) -> list[int]: - Codifica UTF-8 el texto.
- Divide en byte-singletons.
- Por cada merge en orden, reemplaza
(a, b)conab. - Mapea símbolos a IDs.
-
decode(ids: list[int]) -> str: - Mapea IDs a bytes; concatena.
- Decodifica UTF-8 (errors='replace').
Bloque D — save/load¶
-
save(path: Path): vocab.json(víaVocab.save).merges.txt(un merge por línea,bytes(a).hex() bytes(b).hex()).config.json({vocab_size, special_tokens, version}).-
load(path: Path): - Lee los tres ficheros.
- Reconstruye
BPETokenizer.
Bloque E — tests (tests/test_bpe.py)¶
- Implementa cada test de la lista de arriba.
- Usa
hypothesispara el property test. - Todos los tests pasan.
-
pytest --cov src/minitoken≥ 90%.
Bloque F — comprobación de cordura sobre el corpus juguete¶
- Al final de
bpe.pyo enexperiments/11-toy-train/, ejecutaBPETokenizer().train(["I work"]*2 + ["I worked", "he works", "he worked"], vocab_size=261)(256 bytes + 5 merges). - Imprime los merges; verifica que casan con tu traza a mano del lab 00 exactamente.
Restricciones¶
- Solo Python puro + NumPy. Nada de
tiktoken, nada detransformers, nada desentencepiece— según la regla dura 4 del §0 de CLAUDE.md. mypy --strictlimpio.rufflimpio.banditlimpio — sineval, sin shells desubprocesssinshell=False, etc.- Determinismo impuesto vía el fixture de semilla de
tests/conftest.py. - Sin pre-tokenización regex en v1. Solo bytes en bruto.
- Sin símbolos multibyte en líneas de
merges.txt. Serializa como hex.
Condiciones de parada¶
Hecho cuando:
- Los cuatro ficheros comprometidos.
- Todos los tests pasan;
pytest -qestá verde. mypy --strict src/minitoken/limpio.- La traza del corpus juguete del lab 00 se reproduce exactamente vía tu trainer.
pytest -k "property" --hypothesis-seed=42pasa sobre 1000 entradas utf-8 aleatorias.
Escollos¶
- El orden de iteración del hash cambiando tu desempate. Python 3.7+ tiene dicts ordenados por inserción, pero
max(dict, key=...)itera en orden de inserción si múltiples claves empatan. Asegúrate de que el desempate sea sobre la tupla valor(count, pair), sin depender del orden de iteración. - El bucle de merges es demasiado lento. El re-conteo en cada iteración naive está bien para nuestro corpus de 300 KiB pero es agonizante para tests si subes vocab_size a 16k. Optimiza: actualizaciones incrementales de conteo de pares (resta conteos de los vecinos del par fusionado, suma conteos de los vecinos del nuevo par). Opcional; OK dejarlo para la "cuestión abierta" del BLUEPRINT.
- La ida y vuelta falla en el byte
\xff. Probablemente un problema de codificación en save/load — asegúrate de serializar bytes como hex, no como unreprde Python. hypothesisencuentra un contraejemplo. El más común: un par sustituto Unicode (un code point U+D800–U+DFFF en aislamiento). El UTF-16 válido los tiene en pares; el UTF-8 válido no los permite. Decisión: filtrarlos de la estrategia del property test, o usarhypothesis.strategies.text(alphabet=...)para restringir a UTF-8 válido.- Formato de línea
merges.txtequivocado. Común: espacios en el hex de bytes rompen el parser. Usa un separador fijo como\t.
Pista de último recurso¶
Si tras 4 horas el property test de ida y vuelta falla: imprime los primeros 5 fallos que hypothesis encuentra. Lee los bytes. El bug está casi siempre en la codificación (el paso de aplicación de merge), no en la decodificación.
Cuándo consultar solutions/¶
Después de los cuatro ficheros comprometidos y los tests verdes. Solución: solutions/01-implement-bpe-ref.md (apertura de fase) contiene una implementación de referencia con anotaciones línea por línea.
Siguiente lab: lab/02-bpe-on-verb-corpus.md.