Spaces:
Sleeping
Sleeping
File size: 30,381 Bytes
4d362a0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 | """demo interactiva de la órbita de gauge valor-salida.
tres pestañas: la órbita con el conmutador ortogonal/genérico, la
decisión de poda bajo gauge, y qué se mide. toda la aritmética la
ejecuta el código del paper ---`src/gauge_flip.py`,
`scripts/run_fase_G.py`, `scripts/decision_rota.py`--- sobre el
portador ligero de `demo/portador.py`; aquí no se reimplementa nada.
"""
import math
import sys
from pathlib import Path
import gradio as gr
import matplotlib
import torch
from tqdm import tqdm
matplotlib.use("Agg") # sin servidor gráfico en el Space
from matplotlib.figure import Figure # noqa: E402
from matplotlib.patches import Rectangle # noqa: E402
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from demo.portador import (carga, verifica_manifiesto, # noqa: E402
verifica_relleno)
from src.firma_funcional import w_v_columnas # noqa: E402
from src.gauge_flip import (aplica_gauge_ortogonal, # noqa: E402
aplica_gauge_ov)
from src.nucleo_lectura import decisiones, firma_exacta # noqa: E402
DOI = "https://doi.org/10.5281/zenodo.21630534"
# gr.Markdown no renderiza matemáticas salvo que se le declaren los
# delimitadores: sin esto la prosa enseña los dólares en crudo, justo
# al lado de una figura que matplotlib sí compone bien
LATEX = [{"left": "$$", "right": "$$", "display": True},
{"left": "$", "right": "$", "display": False}]
# clave = escala_id (el mando real); el rótulo cambia con el idioma y
# con el locale del número, la clave no
FUERZAS = {128.0: ("muy débil (0,06)", "very weak (0.06)"),
64.0: ("débil (0,13)", "weak (0.13)"),
32.0: ("media (0,25)", "medium (0.25)"),
16.0: ("fuerte (0,50)", "strong (0.50)"),
8.0: ("saturada (1,00)", "saturated (1.00)"),
2.0: ("muy fuerte (4,05)", "very strong (4.05)")}
def op_fuerza(idi: str):
"""opciones del desplegable de fuerza en un idioma.
Args:
idi: 'es' o 'en'.
Returns:
lista de pares (rótulo, valor) para gradio.
"""
j = 0 if idi == "es" else 1
return [(v[j], k) for k, v in FUERZAS.items()]
# por columna: (etiqueta del sector, k de poda, referencia de la tabla 3).
# el k es el 25 % relativo de cada una, y la referencia es la medida
# publicada de esa columna ---nunca la de la otra---
COLUMNAS = {
"vitb": {"es": "ViT-B/16 (visión)", "en": "ViT-B/16 (vision)",
"k": 3, "ref": ("92,7 % / 0,378", "92.7 % / 0.378")},
"pythia": {"es": "Pythia-410M (lenguaje)",
"en": "Pythia-410M (language)",
"k": 4, "ref": ("90,0 % / 0,379", "90.0 % / 0.379")},
}
def op_tipo(idi: str):
"""opciones del mando de tipo de gauge en un idioma.
el valor viaja en clave estable ('gen'/'ort') porque gradio valida
la entrada en el servidor contra las opciones declaradas al
construir el bloque, y `gr.update` no las cambia allí: si el valor
fuese la palabra traducida, el mando quedaría roto en inglés.
Args:
idi: 'es' o 'en'.
Returns:
lista de pares (rótulo, clave) para gradio.
"""
return [(T[idi]["gen"], "gen"), (T[idi]["ort"], "ort")]
def op_columna(idi: str):
"""opciones del desplegable de columna en un idioma.
Args:
idi: 'es' o 'en'.
Returns:
lista de pares (rótulo, clave) para gradio.
"""
return [(v[idi], k) for k, v in COLUMNAS.items()]
# textos en las dos lenguas. el español es el canónico y el inglés su
# derivado, con la terminología del glosario del paper: soft probe,
# hard variant, response signature, static invariant, band, gauge.
# ojo al locale de los números: coma en es, punto en en
T = {
"es": {
"titulo": "# La órbita de gauge valor-salida\nDos modelos "
"idénticos en función, dos geometrías distintas.",
"tab1": "Órbita", "tab2": "Decisión rota", "tab3": "Qué se mide",
"figb": "El abismo, cabeza a cabeza",
"figb_t": "Capa {capa}: lo que el gauge mueve y lo que no",
"figb_1": r"$\Delta v_1(W_O)$ [grados]",
"figb_2": r"$1-|\cos|$ del circuito OV",
"figb_m": "media", "figb_0": "0 exacto",
"figb_s": "suelo de arccos en doble precisión",
"figb_n": "eje común en escala logarítmica: la distancia "
"horizontal entre los dos bloques es el abismo",
"btn3": "Barrer fuerzas",
"figs": "Barrido de fuerzas",
"figs_t": "Capa {capa}, semilla {sem}: deriva contra la "
"fuerza del gauge",
"figs_x": "desviación de R respecto a un múltiplo escalar",
"fig": "Similitud entre cabezas",
"fig_t": "Capa {capa}: |cos| entre las direcciones "
"$v_1(W_O)$ de cada par de cabezas",
"fig_a": "Antes del gauge", "fig_d": "Después del gauge",
"fig_e": "cabeza", "fig_p": "par elegido",
"col": "Columna", "capa": "Capa", "sem": "Semilla del gauge",
"fuerza": "Fuerza (desv. de R al escalar)", "tipo": "Tipo de gauge",
"gen": "genérico", "ort": "ortogonal",
"btn1": "Muestrear gauge", "btn2": "Decidir bajo gauge",
"porcabeza": "Por cabeza",
"cab": ["cabeza", "Δ v1(W_O) [°]", "1-|cos| circuito OV"],
"cab_d": "**{col}, capa {capa}, semilla {sem}, k={k}.**\n",
"res": "**{col}, capa {capa}, gauge {tipo}, semilla {sem}.**\n\n"
"- Desplazamiento medio de $v_1(W_O)$: **{gp}°**\n"
"- Desviación media del circuito OV (invariante), "
"en la métrica que certifica el paper: "
"$1-|\\cos|$ = **{gi}**\n\nMisma libertad de gauge, dos "
"resultados: {ver}. El circuito OV no se mueve en ningún "
"caso ---es el objeto identificable---.",
"ver_ort": "el subgrupo ortogonal **no la mueve**: la lectura "
"por pesos sobrevive intacta",
"ver_gen": "la parte no ortogonal **sí la mueve**, decenas de "
"grados",
"cols_d": ["criterio", "par antes", "par después", "¿cambia?",
"solape"],
"crit_p": "por pesos $v_1(W_O)$", "crit_i": "por el invariante",
"si": "**sí**", "no": "no",
"contador": "**En esta sesión**, la decisión por pesos ha "
"cambiado en {c} de {n} gauges ({pct:.0f} %). La "
"medida publicada para esta columna es {ref} (par / "
"solape); la decisión por el invariante no cambia "
"nunca.",
},
"en": {
"titulo": "# The value-output gauge orbit\nTwo models identical "
"in function, two different geometries.",
"tab1": "Orbit", "tab2": "Broken decision",
"tab3": "What is measured",
"figb": "The gap, head by head",
"figb_t": "Layer {capa}: what the gauge moves and what it "
"does not",
"figb_1": r"$\Delta v_1(W_O)$ [degrees]",
"figb_2": r"$1-|\cos|$ of the OV circuit",
"figb_m": "mean", "figb_0": "exactly 0",
"figb_s": "arccos floor in double precision",
"figb_n": "shared logarithmic axis: the horizontal distance "
"between the two blocks is the gap",
"btn3": "Sweep strengths",
"figs": "Strength sweep",
"figs_t": "Layer {capa}, seed {sem}: drift against gauge "
"strength",
"figs_x": "deviation of R from a scalar multiple",
"fig": "Similarity across heads",
"fig_t": "Layer {capa}: |cos| between the $v_1(W_O)$ "
"directions of every pair of heads",
"fig_a": "Before the gauge", "fig_d": "After the gauge",
"fig_e": "head", "fig_p": "selected pair",
"col": "Column", "capa": "Layer", "sem": "Gauge seed",
"fuerza": "Strength (deviation of R from a scalar)",
"tipo": "Gauge type", "gen": "generic", "ort": "orthogonal",
"btn1": "Sample a gauge", "btn2": "Decide under gauge",
"porcabeza": "Per head",
"cab": ["head", "Δ v1(W_O) [°]", "1-|cos| OV circuit"],
"cab_d": "**{col}, layer {capa}, seed {sem}, k={k}.**\n",
"res": "**{col}, layer {capa}, {tipo} gauge, seed {sem}.**\n\n"
"- Mean displacement of $v_1(W_O)$: **{gp}°**\n"
"- Mean deviation of the OV circuit (invariant), "
"in the metric the paper certifies: "
"$1-|\\cos|$ = **{gi}**\n\nThe same gauge freedom, "
"two outcomes: "
"{ver}. The OV circuit does not move in either case "
"---it is the identifiable object---.",
"ver_ort": "the orthogonal subgroup **does not move it**: the "
"weight reading survives intact",
"ver_gen": "the non-orthogonal part **does move it**, by tens "
"of degrees",
"cols_d": ["criterion", "pair before", "pair after", "changes?",
"overlap"],
"crit_p": "by weights $v_1(W_O)$", "crit_i": "by the invariant",
"si": "**yes**", "no": "no",
"contador": "**In this session**, the decision by weights has "
"changed in {c} of {n} gauges ({pct:.0f} %). The "
"published measurement for this column is {ref} "
"(pair / overlap); the decision by the invariant "
"never changes.",
},
}
# el inglés va primero: es el idioma de partida de la interfaz
IDIOMAS = {"English": "en", "Español": "es"}
def num(x: float, idi: str, dec: int = 3) -> str:
"""formatea un número con el separador decimal del idioma.
Args:
x: valor.
idi: 'es' o 'en'.
dec: decimales.
Returns:
el número como texto, con coma o punto según el idioma.
"""
s = f"{x:.{dec}f}"
return s.replace(".", ",") if idi == "es" else s
_CACHE: dict = {}
verifica_manifiesto() # el código vendido, firmado contra su commit
def cientifico(x: float, idi: str) -> str:
"""notación científica con el separador decimal del idioma.
Args:
x: el valor.
idi: 'es' o 'en'.
Returns:
la cadena formateada.
"""
s = f"{x:.1e}"
return s.replace(".", ",") if idi == "es" else s
def portador(col: str):
"""carga perezosa del portador de una columna, con su contrato.
Args:
col: etiqueta de columna de `COLUMNAS`.
Returns:
el portador ya verificado.
"""
if col not in _CACHE:
p = carga(col)
verifica_relleno(p) # el contrato, vigilado también aquí
_CACHE[col] = p
return _CACHE[col]
def capas_de(col: str):
"""actualiza el desplegable de capas al cambiar de columna.
Args:
col: etiqueta de columna.
Returns:
actualización de gradio con las capas de esa columna.
"""
n = len(portador(col).blocks)
return gr.update(choices=list(range(n)), value=min(5, n - 1))
def _v1_pesos(port, capa: int) -> torch.Tensor:
"""dirección dominante de w_o por cabeza.
Args:
port: portador del sector.
capa: índice de capa.
Returns:
tensor [h, d] de direcciones unitarias.
"""
# sin bajar a simple: el coseno entre dos direcciones
# casi iguales satura en fp32 y el invariante aparece
# moviéndose centésimas de grado que no existen
return firma_exacta(port.w_o_por_cabeza(capa).double(),
"der")
def _v1_invariante(port, capa: int) -> torch.Tensor:
"""dirección dominante del circuito ov, invariante de gauge.
Args:
port: portador del sector.
capa: índice de capa.
Returns:
tensor [h, d] de direcciones unitarias.
"""
w_o = port.w_o_por_cabeza(capa).double()
w_v = w_v_columnas(port, capa, port.n_cabezas,
port.dim_cabeza).double()
r = torch.linalg.qr(w_v, mode="reduced")[1]
return firma_exacta(r @ w_o, "der")
def _con_gauge(col: str, capa: int, semilla: int,
fuerza: float, tipo: str):
"""devuelve una copia del portador con un gauge aplicado.
Args:
capa: capa sobre la que actuar.
semilla: semilla del generador de R.
fuerza: escala del término identidad de R (ignorada si
el gauge es ortogonal, que no tiene intensidad).
tipo: 'gen' u 'ort'.
Returns:
tupla (portador con el gauge aplicado, desviación media de R
respecto a su mejor múltiplo escalar; 0 si es ortogonal, que
no tiene intensidad).
"""
q = portador(col).copia()
if tipo == "ort":
aplica_gauge_ortogonal(q, capa, q.n_cabezas, q.dim_cabeza,
semilla=semilla)
return q, 0.0
desv = aplica_gauge_ov(q, capa, q.n_cabezas, q.dim_cabeza,
semilla=semilla, escala_id=fuerza)
return q, desv
def _mapa_cos(v: torch.Tensor):
"""|cos| entre todas las parejas de direcciones de una capa.
Args:
v: tensor [h, d] de direcciones unitarias.
Returns:
tupla (matriz [h, h] en numpy, par de máximo fuera de la
diagonal).
"""
g = (v @ v.t()).abs().clamp(max=1.0)
m = g.clone()
m.fill_diagonal_(-1.0)
i = int(m.argmax())
return g.numpy(), (i // g.shape[0], i % g.shape[0])
def figura_cos(antes: torch.Tensor, desp: torch.Tensor, capa: int,
idi: str) -> Figure:
"""dibuja la matriz de similitud antes y después del gauge.
es la misma lectura que produce la decisión de poda: la celda más
brillante fuera de la diagonal es el par que se podaría. verla
moverse entre los dos paneles es la afirmación del paper hecha
imagen.
Args:
antes: direcciones [h, d] antes del gauge.
desp: direcciones [h, d] después.
capa: índice de capa, para el título.
idi: 'es' o 'en'.
Returns:
la figura de matplotlib, lista para `gr.Plot`.
"""
d = T[idi]
fig = Figure(figsize=(9.2, 4.2), dpi=110)
ejes = fig.subplots(1, 2)
for ax, x, sub in ((ejes[0], antes, d["fig_a"]),
(ejes[1], desp, d["fig_d"])):
g, par = _mapa_cos(x)
im = ax.imshow(g, cmap="magma", vmin=0.0, vmax=1.0)
for i, j in (par, par[::-1]):
ax.add_patch(Rectangle((j - 0.5, i - 0.5), 1, 1,
fill=False, edgecolor="#39d353",
lw=1.8))
ax.set_title(f"{sub} \u00b7 {d['fig_p']} "
f"{tuple(sorted(par))}",
fontsize=10)
ax.set_xlabel(d["fig_e"], fontsize=9)
ax.set_ylabel(d["fig_e"], fontsize=9)
ax.tick_params(labelsize=8)
fig.colorbar(im, ax=ejes, fraction=0.032, pad=0.02)
fig.suptitle(d["fig_t"].format(capa=capa), fontsize=11)
return fig
def figura_abismo(gp: torch.Tensor, gi: torch.Tensor, capa: int,
idi: str) -> Figure:
"""dibuja la separación entre lo que se mueve y lo que no.
la tabla por cabeza dice que $v_1(W_O)$ deriva decenas de grados y
que el circuito OV se queda en 1e-16, pero eso hay que leerlo. con
los dos bloques de barras sobre un mismo eje logarítmico, el
abismo ---dieciocho órdenes de magnitud--- es distancia en la
pantalla. bajo el conmutador ortogonal las barras de arriba
desaparecen y las de abajo no se inmutan: c1 en una imagen.
Args:
gp: desplazamiento de v1(w_o) por cabeza, en grados.
gi: desviación 1-|cos| del circuito ov por cabeza.
capa: índice de capa, para el título.
idi: 'es' o 'en'.
Returns:
la figura de matplotlib, lista para `gr.Plot`.
"""
d = T[idi]
n = gp.shape[0]
y = list(range(n))
fig = Figure(figsize=(9.2, 0.9 + 0.42 * n), dpi=110)
ejes = fig.subplots(2, 1, sharex=True)
for ax, v, etq, color in ((ejes[0], gp, d["figb_1"], "#c4432b"),
(ejes[1], gi, d["figb_2"], "#2b6cc4")):
ax.barh(y, v.numpy(), height=0.72, color=color)
# un valor exactamente nulo no dibuja barra en escala
# logarítmica y se leería como dato ausente; se rotula, que es
# justo lo que ocurre con el gauge ortogonal en el panel de
# arriba: cero exacto, no falta de medida
for h in y:
if float(v[h]) == 0.0:
ax.text(2e-18, h, d["figb_0"], va="center",
ha="left", fontsize=7, color=color)
ax.set_ylabel(d["fig_e"], fontsize=9)
ax.set_yticks(y)
ax.set_yticklabels([str(h) for h in y], fontsize=7)
ax.invert_yaxis()
ax.grid(axis="x", ls=":", lw=0.6, alpha=0.5)
ax.set_title(f"{etq} · {d['figb_m']} "
f"{float(v.mean()):.2e}", fontsize=10)
# el panel de grados tiene suelo: arccos cerca de 1 pierde la
# mitad de los dígitos, así que sqrt(2*eps) rad es lo mínimo
# medible. sin la línea, las barras del gauge ortogonal ---que
# deben ser cero--- se leerían como una deriva de 1e-6 grados
suelo = math.degrees(math.sqrt(2 * torch.finfo(torch.float64).eps))
ejes[0].axvline(suelo, ls="--", lw=1.0, color="#555555")
ejes[0].text(suelo * 1.4, n - 0.4, d["figb_s"],
fontsize=7, color="#555555", va="center")
# el eje común es lo que convierte dos medidas en un abismo; el
# suelo baja hasta la precisión de máquina para que el bloque de
# abajo tenga dónde caber
ejes[1].set_xscale("log")
ejes[1].set_xlim(1e-18, 1e3)
ejes[1].set_xlabel(d["figb_n"], fontsize=8)
fig.suptitle(d["figb_t"].format(capa=capa), fontsize=11)
fig.tight_layout()
return fig
def barrido(idioma: str, col: str, capa: int, semilla: int) -> Figure:
"""recorre las seis fuerzas y dibuja la deriva contra cada una.
es la tabla 2 del paper dibujándose en vivo: la deriva de
$v_1(W_O)$ crece con la desviación de R respecto a un escalar,
mientras el circuito OV se queda plano en el suelo de la máquina.
cuesta seis gauges por pulsación, y por eso tiene botón propio.
Args:
idioma: rótulo del selector.
col: clave de columna.
capa: capa a interrogar.
semilla: semilla del gauge.
Returns:
la figura de matplotlib, lista para `gr.Plot`.
"""
idi = IDIOMAS[idioma]
d = T[idi]
p = portador(col)
antes_p, antes_i = _v1_pesos(p, capa), _v1_invariante(p, capa)
xs, ys_p, ys_i = [], [], []
for escala in tqdm(sorted(FUERZAS, reverse=True),
desc=f"barrido {col} L{capa}", leave=False):
q, desv = _con_gauge(col, capa, semilla, escala, "gen")
cp = _cos_abs(antes_p, _v1_pesos(q, capa))
ci = _cos_abs(antes_i, _v1_invariante(q, capa))
xs.append(desv)
ys_p.append(float(torch.rad2deg(torch.arccos(cp)).mean()))
ys_i.append(float((1.0 - ci).mean()))
fig = Figure(figsize=(8.4, 5.4), dpi=110)
ejes = fig.subplots(2, 1, sharex=True)
# dos paneles y no un eje común: sobre veintiuna décadas el
# crecimiento de v1 ---de veinte a ochenta grados--- sería
# invisible. arriba la subida, abajo la planitud, cada una en su
# escala; el abismo entre ambas lo cuenta la otra figura
ejes[0].plot(xs, ys_p, "o-", color="#c4432b")
ejes[0].set_ylim(0.0, 92.0)
ejes[0].set_ylabel(d["figb_1"], fontsize=9)
ejes[1].plot(xs, ys_i, "s-", color="#2b6cc4")
ejes[1].set_yscale("log")
ejes[1].set_ylim(1e-18, 1e-12)
ejes[1].set_ylabel(d["figb_2"], fontsize=9)
ejes[1].set_xscale("log")
ejes[1].set_xlabel(d["figs_x"], fontsize=9)
for ax in ejes:
ax.grid(ls=":", lw=0.6, alpha=0.5)
ax.tick_params(labelsize=8)
fig.suptitle(d["figs_t"].format(capa=capa, sem=semilla),
fontsize=11)
fig.tight_layout()
return fig
def _cos_abs(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
"""|cos| entre direcciones emparejadas por cabeza.
Args:
a: direcciones [h, d].
b: direcciones [h, d].
Returns:
tensor [h] con el coseno en valor absoluto, acotado a 1.
"""
return (a * b).sum(1).abs().clamp(max=1.0)
def orbita(idioma: str, col: str, capa: int, semilla: int,
fuerza: float, tipo: str):
"""mide cuánto mueve el gauge la lectura por pesos y el invariante.
Args:
capa: capa a interrogar.
semilla: semilla del gauge.
fuerza: escala del término identidad de R.
tipo: 'gen' u 'ort'.
Returns:
tupla (resumen en markdown, tabla por cabeza, figura del
abismo).
"""
p = portador(col)
antes_p, antes_i = _v1_pesos(p, capa), _v1_invariante(p, capa)
q, _ = _con_gauge(col, capa, semilla, fuerza, tipo)
desp_p, desp_i = _v1_pesos(q, capa), _v1_invariante(q, capa)
# v1 en grados, que es la magnitud interpretable; el invariante en
# 1-|cos|, porque arccos cerca de 1 pierde la mitad de los dígitos
# y pondría un suelo de ~1e-6 grados donde no hay movimiento
gp = torch.rad2deg(torch.arccos(_cos_abs(antes_p, desp_p)))
gi = 1.0 - _cos_abs(antes_i, desp_i)
d = T[IDIOMAS[idioma]]
idi = IDIOMAS[idioma]
filas = [[h, num(float(gp[h]), idi, 2),
cientifico(float(gi[h]), idi)]
for h in range(p.n_cabezas)]
ver = d["ver_ort"] if tipo == "ort" else d["ver_gen"]
cient = cientifico(float(gi.mean()), idi)
resumen = d["res"].format(
col=COLUMNAS[col][idi], capa=capa, tipo=d[tipo],
sem=semilla,
gp=num(float(gp.mean()), idi, 2),
gi=cient, ver=ver)
return resumen, filas, figura_abismo(gp, gi, capa, idi)
def decision(idioma: str, col: str, capa: int, semilla: int,
fuerza: float, estado: dict):
"""materializa la decisión de poda antes y después del gauge.
Args:
capa: capa a interrogar.
semilla: semilla del gauge.
fuerza: escala del término identidad de R.
estado: contador acumulado de la sesión.
Returns:
tupla (markdown, estado actualizado, mapa de similitud).
"""
p = portador(col)
idi = IDIOMAS[idioma]
d = T[idi]
k = COLUMNAS[col]["k"]
ref = COLUMNAS[col]["ref"][0 if idi == "es" else 1]
nombre_col = COLUMNAS[col][idi]
q, _ = _con_gauge(col, capa, semilla, fuerza, "gen")
lineas = [d["cab_d"].format(col=nombre_col, capa=capa,
sem=semilla, k=k),
"| " + " | ".join(d["cols_d"]) + " |",
"|---|---|---|---|---|"]
for nombre, f in ((d["crit_p"], _v1_pesos),
(d["crit_i"], _v1_invariante)):
par0, top0 = decisiones(f(p, capa), k)
par1, top1 = decisiones(f(q, capa), k)
sol = len(set(top0) & set(top1)) / k
cambia = par1 != par0
if nombre == d["crit_p"]:
estado["n"] = estado.get("n", 0) + 1
estado["c"] = estado.get("c", 0) + int(cambia)
lineas.append(f"| {nombre} | {par0} | {par1} | "
f"{d['si'] if cambia else d['no']} | "
f"{num(sol, idi, 2)} |")
pct = 100 * estado["c"] / estado["n"]
lineas.append("")
lineas.append(d["contador"].format(c=estado["c"],
n=estado["n"], pct=pct,
ref=ref))
# el mapa vive aquí y no en la órbita: es la lectura de la que
# sale esta tabla, y verlo debajo convierte la fila «cambia: sí»
# en algo comprobable en vez de en un acto de fe
mapa = figura_cos(_v1_pesos(p, capa), _v1_pesos(q, capa),
capa, idi)
return "\n".join(lineas), estado, mapa
QSM = {"es": f"""
### Qué se mide aquí
Una cabeza de atención escribe en el flujo residual a través de dos
matrices, $W_v$ y $W_O$. Esa factorización **no es única**: para
cualquier $R$ invertible, sustituir $W_v \\to W_v R$ y
$W_O \\to R^{{-1}} W_O$ deja la función del modelo exactamente igual,
porque el producto $W_v W_O$ ---el circuito OV--- no cambia.
La dirección dominante $v_1(W_O)$ **no** es función de ese producto,
así que se mueve con $R$ mientras el modelo calcula lo mismo: su
órbita bajo el gauge es la esfera unitaria completa de su espacio
fila, y por tanto **ningún umbral de similitud sobre esa dirección
admite radio certificado positivo**.
Un matiz que la primera pestaña enseña: el subgrupo **ortogonal** deja
$v_1(W_O)$ quieta. Es la parte **no ortogonal** del gauge la que la
mueve, y está presente en cualquier reparametrización genérica.
Los pesos son los de un ViT-B/16 afinado en ImageNet-100 (semilla 42)
y los de Pythia-410M sin entrenar nada. Cada gauge muestreado es
reproducible: basta repetir la semilla.
Código, datos y certificación de la órbita: <{DOI}>
""", "en": f"""
### What is measured here
An attention head writes into the residual stream through two
matrices, $W_v$ and $W_O$. That factorization is **not unique**: for
any invertible $R$, substituting $W_v \\to W_v R$ and
$W_O \\to R^{{-1}} W_O$ leaves the model's function exactly as it was,
because the product $W_v W_O$ ---the OV circuit--- does not change.
The dominant direction $v_1(W_O)$ is **not** a function of that
product, so it moves with $R$ while the model computes the same
thing: its orbit under the gauge is the full unit sphere of its row
space, and therefore **no similarity threshold on that direction
admits a positive certified radius**.
One nuance the first tab shows: the **orthogonal** subgroup leaves
$v_1(W_O)$ still. It is the **non-orthogonal** part of the gauge that
moves it, and it is present in any generic reparametrization.
The weights are those of a ViT-B/16 fine-tuned on ImageNet-100
(seed 42) and of Pythia-410M with no training at all. Every sampled
gauge is reproducible: just repeat the seed.
Code, data and the orbit certification: <{DOI}>
"""}
def cambia_idioma(idioma: str):
"""rehace etiquetas y textos estáticos en el idioma elegido.
Args:
idioma: clave de `IDIOMAS`.
Returns:
tupla de actualizaciones de gradio, en el orden de los
componentes que dependen del idioma (rótulos de pestaña
incluidos, al final).
"""
idi = IDIOMAS[idioma]
d = T[idi]
col = gr.update(choices=op_columna(idi), label=d["col"])
fue = gr.update(choices=op_fuerza(idi), value=8.0, label=d["fuerza"])
return (gr.update(value=d["titulo"]),
col, gr.update(label=d["capa"]),
gr.update(label=d["sem"]), fue,
gr.update(choices=op_tipo(idi), value="gen",
label=d["tipo"]),
gr.update(value=d["btn1"]), gr.update(value=d["btn3"]),
gr.update(label=d["figs"]),
gr.update(label=d["figb"]),
gr.update(headers=d["cab"], label=d["porcabeza"]),
col, gr.update(label=d["capa"]),
gr.update(label=d["sem"]), fue,
gr.update(value=d["btn2"]),
gr.update(label=d["fig"]),
gr.update(value=QSM[idi]),
gr.update(label=d["tab1"]), gr.update(label=d["tab2"]),
gr.update(label=d["tab3"]))
TITULO = "The value-output gauge orbit - La órbita de gauge "\
"valor-salida"
with gr.Blocks(title=TITULO) as demo:
idi = gr.Radio(list(IDIOMAS), value="English", label="Idioma / Language")
cab = gr.Markdown(T["en"]["titulo"], latex_delimiters=LATEX)
with gr.Tab(T["en"]["tab1"]) as pes1:
with gr.Row():
col_o = gr.Dropdown(op_columna("en"), value="vitb",
label=T["en"]["col"])
capa_o = gr.Dropdown(list(range(12)), value=5,
label=T["en"]["capa"])
sem_o = gr.Number(value=0, precision=0, label=T["en"]["sem"])
fue_o = gr.Dropdown(op_fuerza("en"), value=8.0,
label=T["en"]["fuerza"])
tip_o = gr.Radio(op_tipo("en"), value="gen",
label=T["en"]["tipo"])
with gr.Row():
btn_o = gr.Button(T["en"]["btn1"], variant="primary")
btn_s = gr.Button(T["en"]["btn3"])
# el barrido va pegado a su botón y en un desplegable que se
# abre solo: con la salida al final de la pestaña, debajo de
# la tabla por cabeza, quedaba fuera de pantalla y el botón
# parecía no hacer nada
with gr.Accordion(T["en"]["figs"], open=False) as ple_s:
fig_s = gr.Plot(show_label=False)
res_o = gr.Markdown(latex_delimiters=LATEX)
fig_o = gr.Plot(label=T["en"]["figb"])
tab_o = gr.Dataframe(headers=T["en"]["cab"],
label=T["en"]["porcabeza"])
col_o.change(capas_de, col_o, capa_o)
btn_o.click(orbita, [idi, col_o, capa_o, sem_o, fue_o, tip_o],
[res_o, tab_o, fig_o])
btn_s.click(barrido, [idi, col_o, capa_o, sem_o], fig_s).then(
lambda: gr.update(open=True), None, ple_s)
with gr.Tab(T["en"]["tab2"]) as pes2:
with gr.Row():
col_d = gr.Dropdown(op_columna("en"), value="vitb",
label=T["en"]["col"])
capa_d = gr.Dropdown(list(range(12)), value=5,
label=T["en"]["capa"])
sem_d = gr.Number(value=0, precision=0, label=T["en"]["sem"])
fue_d = gr.Dropdown(op_fuerza("en"), value=8.0,
label=T["en"]["fuerza"])
btn_d = gr.Button(T["en"]["btn2"], variant="primary")
res_d = gr.Markdown(latex_delimiters=LATEX)
fig_m = gr.Plot(label=T["en"]["fig"])
est = gr.State({})
col_d.change(capas_de, col_d, capa_d)
btn_d.click(decision, [idi, col_d, capa_d, sem_d, fue_d, est],
[res_d, est, fig_m])
with gr.Tab(T["en"]["tab3"]) as pes3:
qsm = gr.Markdown(QSM["en"], latex_delimiters=LATEX)
idi.change(cambia_idioma, idi,
[cab, col_o, capa_o, sem_o, fue_o, tip_o, btn_o, btn_s,
ple_s, fig_o, tab_o, col_d, capa_d, sem_d, fue_d,
btn_d, fig_m, qsm, pes1, pes2, pes3])
if __name__ == "__main__":
demo.launch()
|