Phase 6: train a second LoRA for real Penpot UI design capability #5
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+105
-41
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+113
-28
@@ -6,7 +6,7 @@ Corre LOCALMENTE (sin GPU, sin modelo, sin tokenizer). Determinista.
|
|||||||
|
|
||||||
Produce tres archivos NUEVOS -- nunca toca data/train.jsonl ni data/eval.jsonl:
|
Produce tres archivos NUEVOS -- nunca toca data/train.jsonl ni data/eval.jsonl:
|
||||||
|
|
||||||
data/train_lora2.jsonl 900 ejemplos
|
data/train_lora2.jsonl 900 ejemplos (mezcla de 1000, partida 90/10)
|
||||||
data/eval_lora2.jsonl ~100 ejemplos, estratificado por porcion
|
data/eval_lora2.jsonl ~100 ejemplos, estratificado por porcion
|
||||||
data/calibration_v2.jsonl 256 ejemplos para la calibracion NVFP4
|
data/calibration_v2.jsonl 256 ejemplos para la calibracion NVFP4
|
||||||
|
|
||||||
@@ -69,7 +69,14 @@ EVAL_OUT = Path(os.environ.get("EVAL_LORA2_OUT", REPO_ROOT / "data" / "eval_lora
|
|||||||
CALIB_OUT = Path(os.environ.get("CALIB_V2_OUT", REPO_ROOT / "data" / "calibration_v2.jsonl"))
|
CALIB_OUT = Path(os.environ.get("CALIB_V2_OUT", REPO_ROOT / "data" / "calibration_v2.jsonl"))
|
||||||
|
|
||||||
SEED = 6006
|
SEED = 6006
|
||||||
N_TRAIN = 900
|
|
||||||
|
# La mezcla se arma con 1000 ejemplos y se parte 90/10, de modo que TRAIN queda en exactamente
|
||||||
|
# 900. Ese 900 no es cosmetico: 900/16 = 56.25 pasos por epoca x 3 epocas = 168 pasos, que el
|
||||||
|
# plan aparea deliberadamente con los 166 de la Fase 3 para que la longitud de la trayectoria
|
||||||
|
# del optimizador sea comparable. Armar 900 y despues sacarle el eval dejaria 811 -> 152 pasos,
|
||||||
|
# y la comparacion se rompe sin que nada avise.
|
||||||
|
N_MIX = 1000
|
||||||
|
N_TRAIN_ESPERADO = 900
|
||||||
EVAL_FRACTION = 0.10
|
EVAL_FRACTION = 0.10
|
||||||
N_CALIBRATION = 256
|
N_CALIBRATION = 256
|
||||||
|
|
||||||
@@ -88,22 +95,24 @@ PORTION_OF_GROUP = {
|
|||||||
"D": "api_recuperacion",
|
"D": "api_recuperacion",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Los targets son sobre la mezcla de 1000; las proporciones son las mismas que documenta el
|
||||||
|
# docstring (33.3 / 6.7 / 4.4 / 2.2 y 53.3 de replay).
|
||||||
PORTION_TARGETS = {
|
PORTION_TARGETS = {
|
||||||
"diseno": 300,
|
"diseno": 333,
|
||||||
"api_forma_correcta": 60,
|
"api_forma_correcta": 67,
|
||||||
"api_recuperacion": 40,
|
"api_recuperacion": 44,
|
||||||
"api_preguntar": 20,
|
"api_preguntar": 23,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Replay: bucket de origen en data/train.jsonl -> cuantos ejemplos tomar.
|
# Replay: bucket de origen en data/train.jsonl -> cuantos ejemplos tomar.
|
||||||
REPLAY_TARGETS = {
|
REPLAY_TARGETS = {
|
||||||
"replay": 180,
|
"replay": 200,
|
||||||
"otros_mcps": 120,
|
"otros_mcps": 133,
|
||||||
"skills_adherencia": 70,
|
"skills_adherencia": 78,
|
||||||
"negativos_delegacion_errores": 65, # union de los tres buckets chicos
|
"negativos_delegacion_errores": 72, # union de los tres buckets chicos
|
||||||
}
|
}
|
||||||
REPLAY_UNION_BUCKETS = ("negativos", "delegacion_subagentes", "manejo_errores")
|
REPLAY_UNION_BUCKETS = ("negativos", "delegacion_subagentes", "manejo_errores")
|
||||||
N_PENPOT_BASICOS = 45
|
N_PENPOT_BASICOS = 50
|
||||||
|
|
||||||
# Patrones prohibidos. La fuente de verdad es scripts/07_lint_penpot_code.py; aca se re-declaran
|
# Patrones prohibidos. La fuente de verdad es scripts/07_lint_penpot_code.py; aca se re-declaran
|
||||||
# los que aplican al SCAN de replay (no hace falta el set completo: lo que se busca es descartar
|
# los que aplican al SCAN de replay (no hace falta el set completo: lo que se busca es descartar
|
||||||
@@ -130,20 +139,89 @@ def load_jsonl(path):
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def has_forbidden(example):
|
def code_payloads(example):
|
||||||
"""True si el ejemplo serializado contiene algun patron de la API vieja."""
|
"""Los payloads de `code` del ejemplo, que es lo unico donde un patron prohibido es un bug."""
|
||||||
blob = json.dumps(example, ensure_ascii=False)
|
out = []
|
||||||
for _, rx in FORBIDDEN_REPLAY:
|
for msg in example.get("messages", []):
|
||||||
if rx.search(blob):
|
for tc in msg.get("tool_calls") or []:
|
||||||
return True
|
args = tc.get("function", {}).get("arguments")
|
||||||
return False
|
if isinstance(args, dict) and "code" in args:
|
||||||
|
out.append(args["code"])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def forbidden_hits(example):
|
def forbidden_hits(example):
|
||||||
blob = json.dumps(example, ensure_ascii=False)
|
"""Patrones de la API vieja presentes en el CODIGO del ejemplo.
|
||||||
|
|
||||||
|
Se escanea solo el `code`, nunca el ejemplo serializado entero. Escanear la prosa marca
|
||||||
|
como infractores justo a los seeds correctivos: el que arranca con el usuario pidiendo
|
||||||
|
"Importa esta imagen con import_image", el que cita verbatim la linea del overview que
|
||||||
|
menciona `import_image`, el que explica que `board.layout` no existe, el que aclara que
|
||||||
|
`filePath` esta eliminado del schema. Todos ellos NOMBRAN la API equivocada precisamente
|
||||||
|
para ensenar a no usarla; bloquearlos seria bloquear la correccion.
|
||||||
|
"""
|
||||||
|
blob = "\n".join(code_payloads(example))
|
||||||
return [name for name, rx in FORBIDDEN_REPLAY if rx.search(blob)]
|
return [name for name, rx in FORBIDDEN_REPLAY if rx.search(blob)]
|
||||||
|
|
||||||
|
|
||||||
|
def has_forbidden(example):
|
||||||
|
return bool(forbidden_hits(example))
|
||||||
|
|
||||||
|
|
||||||
|
def error_strings():
|
||||||
|
"""Los strings de error verbatim de data/schemas/penpot_errors.md."""
|
||||||
|
text = (REPO_ROOT / "data" / "schemas" / "penpot_errors.md").read_text(encoding="utf-8")
|
||||||
|
out = set()
|
||||||
|
for line in text.splitlines():
|
||||||
|
if line.startswith("|"):
|
||||||
|
for cell in line.strip().strip("|").split("|"):
|
||||||
|
out.update(re.findall(r"`([^`]+)`", cell))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
ERROR_STRINGS = error_strings()
|
||||||
|
|
||||||
|
|
||||||
|
def unexplained_forbidden(example):
|
||||||
|
"""Patrones de la API vieja que NO son un error intencional corregido.
|
||||||
|
|
||||||
|
Misma regla mecanica que scripts/07_lint_penpot_code.py: un payload puede traer un patron
|
||||||
|
prohibido si y solo si (a) su tool result es un string de error real de la allow-list, y
|
||||||
|
(b) un payload POSTERIOR del mismo ejemplo hace lo mismo SIN el patron. O sea: el error
|
||||||
|
tiene que haber ocurrido de verdad y tiene que haber sido corregido.
|
||||||
|
|
||||||
|
Se deriva del contenido, no de la porcion ni de una bandera en `meta`. Apoyarse en la
|
||||||
|
porcion se rompe en cuanto un seed de recuperacion de error vive en un grupo que no es el
|
||||||
|
de recuperacion -- que es exactamente el caso del seed de aridad de `findShapeById` del
|
||||||
|
grupo A1, cuyo lugar natural es la familia de busqueda.
|
||||||
|
"""
|
||||||
|
msgs = example.get("messages", [])
|
||||||
|
fallidas = {
|
||||||
|
m.get("tool_call_id")
|
||||||
|
for m in msgs
|
||||||
|
if m.get("role") == "tool" and (m.get("content") or "").strip() in ERROR_STRINGS
|
||||||
|
}
|
||||||
|
llamadas = []
|
||||||
|
for m in msgs:
|
||||||
|
for tc in m.get("tool_calls") or []:
|
||||||
|
args = tc.get("function", {}).get("arguments")
|
||||||
|
if isinstance(args, dict) and "code" in args:
|
||||||
|
llamadas.append((tc.get("id"), args["code"]))
|
||||||
|
|
||||||
|
sin_explicar = []
|
||||||
|
for nombre, rx in FORBIDDEN_REPLAY:
|
||||||
|
for idx, (cid, code) in enumerate(llamadas):
|
||||||
|
if not rx.search(code):
|
||||||
|
continue
|
||||||
|
corregido = cid in fallidas and any(
|
||||||
|
not rx.search(c) for _, c in llamadas[idx + 1:]
|
||||||
|
)
|
||||||
|
if not corregido:
|
||||||
|
sin_explicar.append(nombre)
|
||||||
|
break
|
||||||
|
return sin_explicar
|
||||||
|
|
||||||
|
|
||||||
def paraphrase_variants(seed_ex):
|
def paraphrase_variants(seed_ex):
|
||||||
"""Expande un seed en sus variantes de prompt.
|
"""Expande un seed en sus variantes de prompt.
|
||||||
|
|
||||||
@@ -320,9 +398,9 @@ def main():
|
|||||||
mixed.extend(sample_replay(rows, target, rng, label))
|
mixed.extend(sample_replay(rows, target, rng, label))
|
||||||
|
|
||||||
# ---- 4. verificaciones duras -----------------------------------------------------------
|
# ---- 4. verificaciones duras -----------------------------------------------------------
|
||||||
if len(mixed) != N_TRAIN:
|
if len(mixed) != N_MIX:
|
||||||
raise SystemExit(
|
raise SystemExit(
|
||||||
f"[ABORT] la mezcla tiene {len(mixed)} ejemplos, se esperaban {N_TRAIN}. "
|
f"[ABORT] la mezcla tiene {len(mixed)} ejemplos, se esperaban {N_MIX}. "
|
||||||
f"Revisar PORTION_TARGETS / REPLAY_TARGETS / N_PENPOT_BASICOS."
|
f"Revisar PORTION_TARGETS / REPLAY_TARGETS / N_PENPOT_BASICOS."
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -330,19 +408,19 @@ def main():
|
|||||||
# que se identifican mecanicamente: su porcion es 'api_recuperacion'.
|
# que se identifican mecanicamente: su porcion es 'api_recuperacion'.
|
||||||
offenders = []
|
offenders = []
|
||||||
for i, ex in enumerate(mixed):
|
for i, ex in enumerate(mixed):
|
||||||
if ex["meta"].get("porcion") == "api_recuperacion":
|
hits = unexplained_forbidden(ex)
|
||||||
continue
|
|
||||||
hits = forbidden_hits(ex)
|
|
||||||
if hits:
|
if hits:
|
||||||
offenders.append((i, ex["meta"].get("porcion"), hits))
|
offenders.append((i, ex["meta"].get("porcion"), hits))
|
||||||
if offenders:
|
if offenders:
|
||||||
print(f"\n[ABORT] {len(offenders)} ejemplo(s) de la mezcla final contienen patrones de la "
|
print(f"\n[ABORT] {len(offenders)} ejemplo(s) de la mezcla final usan la API vieja en su "
|
||||||
f"API vieja fuera de la porcion de recuperacion de error:")
|
f"`code` sin que sea un error real corregido despues:")
|
||||||
for i, portion, hits in offenders[:20]:
|
for i, portion, hits in offenders[:20]:
|
||||||
print(f" #{i} (porcion={portion}): {hits}")
|
print(f" #{i} (porcion={portion}): {hits}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
print("[OK] cero patrones de la API vieja en la mezcla final "
|
n_intencionales = sum(1 for ex in mixed if forbidden_hits(ex))
|
||||||
"(fuera de la porcion de recuperacion de error, donde son el material didactico)")
|
print(f"[OK] cero usos injustificados de la API vieja en la mezcla final "
|
||||||
|
f"({n_intencionales} ejemplos la contienen como error real corregido despues, que es "
|
||||||
|
f"el material didactico de recuperacion)")
|
||||||
|
|
||||||
# Guarda anti-colapso, analoga a la de 05_build_dataset.py. Sin ella, la porcion nueva puede
|
# Guarda anti-colapso, analoga a la de 05_build_dataset.py. Sin ella, la porcion nueva puede
|
||||||
# ser 420 ejemplos que son 96 trayectorias repetidas 4.4 veces cada una, y nada lo diria: el
|
# ser 420 ejemplos que son 96 trayectorias repetidas 4.4 veces cada una, y nada lo diria: el
|
||||||
@@ -384,6 +462,13 @@ def main():
|
|||||||
# ---- 5. split y escritura ---------------------------------------------------------------
|
# ---- 5. split y escritura ---------------------------------------------------------------
|
||||||
split_rng = random.Random(SEED)
|
split_rng = random.Random(SEED)
|
||||||
train, eval_ = stratified_split(mixed, split_rng)
|
train, eval_ = stratified_split(mixed, split_rng)
|
||||||
|
if len(train) != N_TRAIN_ESPERADO:
|
||||||
|
print(f"[WARN] train quedo en {len(train)} y no en {N_TRAIN_ESPERADO}: los pasos de "
|
||||||
|
f"entrenamiento van a ser {round(len(train) / 16 * 3)} en vez de 168. Es por el "
|
||||||
|
f"redondeo del split por porcion; ajustar PORTION_TARGETS/REPLAY_TARGETS si "
|
||||||
|
f"importa la comparabilidad exacta con la Fase 3.")
|
||||||
|
print(f"[INFO] pasos de entrenamiento proyectados: {len(train)}/16 x 3 epocas = "
|
||||||
|
f"{round(len(train) / 16 * 3)}")
|
||||||
write_jsonl(TRAIN_OUT, train)
|
write_jsonl(TRAIN_OUT, train)
|
||||||
write_jsonl(EVAL_OUT, eval_)
|
write_jsonl(EVAL_OUT, eval_)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user