Spaces:
Sleeping
Sleeping
| """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() | |