diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..580d310c2d211aca166288a207e972ced9bb0100 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,35 +1 @@ -*.7z filter=lfs diff=lfs merge=lfs -text -*.arrow filter=lfs diff=lfs merge=lfs -text -*.bin filter=lfs diff=lfs merge=lfs -text -*.bz2 filter=lfs diff=lfs merge=lfs -text -*.ckpt filter=lfs diff=lfs merge=lfs -text -*.ftz filter=lfs diff=lfs merge=lfs -text -*.gz filter=lfs diff=lfs merge=lfs -text -*.h5 filter=lfs diff=lfs merge=lfs -text -*.joblib filter=lfs diff=lfs merge=lfs -text -*.lfs.* filter=lfs diff=lfs merge=lfs -text -*.mlmodel filter=lfs diff=lfs merge=lfs -text -*.model filter=lfs diff=lfs merge=lfs -text -*.msgpack filter=lfs diff=lfs merge=lfs -text -*.npy filter=lfs diff=lfs merge=lfs -text -*.npz filter=lfs diff=lfs merge=lfs -text -*.onnx filter=lfs diff=lfs merge=lfs -text -*.ot filter=lfs diff=lfs merge=lfs -text -*.parquet filter=lfs diff=lfs merge=lfs -text -*.pb filter=lfs diff=lfs merge=lfs -text -*.pickle filter=lfs diff=lfs merge=lfs -text -*.pkl filter=lfs diff=lfs merge=lfs -text -*.pt filter=lfs diff=lfs merge=lfs -text -*.pth filter=lfs diff=lfs merge=lfs -text -*.rar filter=lfs diff=lfs merge=lfs -text *.safetensors filter=lfs diff=lfs merge=lfs -text -saved_model/**/* filter=lfs diff=lfs merge=lfs -text -*.tar.* filter=lfs diff=lfs merge=lfs -text -*.tar filter=lfs diff=lfs merge=lfs -text -*.tflite filter=lfs diff=lfs merge=lfs -text -*.tgz filter=lfs diff=lfs merge=lfs -text -*.wasm filter=lfs diff=lfs merge=lfs -text -*.xz filter=lfs diff=lfs merge=lfs -text -*.zip filter=lfs diff=lfs merge=lfs -text -*.zst filter=lfs diff=lfs merge=lfs -text -*tfevents* filter=lfs diff=lfs merge=lfs -text diff --git a/README.md b/README.md index e15217f450c30641a76cf4cbeac436bbce6059c2..f4866acdd244d1fabcc21a62cbb47f2d72bedd4d 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,31 @@ --- -title: v1(W_O) is not identifiable -emoji: 🎛️ -colorFrom: purple -colorTo: red -sdk: static +title: The value-output gauge orbit +emoji: 🧭 +colorFrom: indigo +colorTo: gray +sdk: gradio +sdk_version: 6.22.0 +app_file: app.py pinned: false -license: apache-2.0 --- -# The dominant direction of $W_O$ is not identifiable +# The value-output gauge orbit -Interactive demo (precomputed gallery, static) for *"Same function, -different pruning: the dominant direction of $W_O$ under free, soft, -and hard intervention"*. +Interactive demo. The dominant direction of a head's output projection +moves under a reparameterisation that leaves the model's function +untouched, and the pruning decision it yields moves with it. The +identifiable object ---the OV circuit--- does not move. -Code: -Results: -Checkpoints: -DOI: [10.5281/zenodo.21630535](https://doi.org/10.5281/zenodo.21630535) +Two models, identical in function, two different prunings. English and +Spanish; ViT-B/16 and Pythia-410M. -Browse 72 precomputed combinations (12 layers × 3 gauge strengths × -2 random seeds), all from the real ViT-B/16 seed-42 base checkpoint -(anchor column of the paper), showing that a value-output gauge -transformation moves each head's dominant weight direction $v_1(W_O)$ -at will —even flipping which pair of heads looks "most redundant"— -while the OV circuit $W_v W_O$, the only quantity the model's function -actually depends on, stays fixed to machine precision. +DOI: https://doi.org/10.5281/zenodo.21630534 + +--- + +# La órbita de gauge valor-salida + +Demo interactiva. La dirección dominante de la proyección de salida de +una cabeza se desplaza bajo una reparametrización que deja la función +del modelo intacta, y la decisión de poda que produce cambia con ella. +El objeto identificable ---el circuito OV--- no se mueve. diff --git a/app.py b/app.py new file mode 100644 index 0000000000000000000000000000000000000000..b00149b88e61c85f120b56a6bcb4a2bdb78436dd --- /dev/null +++ b/app.py @@ -0,0 +1,753 @@ +"""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() diff --git a/artifacts/demo/sector_vo_pythia.safetensors b/artifacts/demo/sector_vo_pythia.safetensors new file mode 100644 index 0000000000000000000000000000000000000000..294afa9b65c230af2f18de6776466032574f9d56 --- /dev/null +++ b/artifacts/demo/sector_vo_pythia.safetensors @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65ce0d4a55d05a741cdc3333736cc106b10fc2382d7048ca823bd612b62c8d59 +size 201518424 diff --git a/artifacts/demo/sector_vo_vitb.safetensors b/artifacts/demo/sector_vo_vitb.safetensors new file mode 100644 index 0000000000000000000000000000000000000000..71e5673a1b68ea4be242a71e4d8a64cf0026cbf0 --- /dev/null +++ b/artifacts/demo/sector_vo_vitb.safetensors @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c17db69cdd7813d98b126767c8836e9b58cc51e8ff70ae302d8a89d653f05823 +size 56693928 diff --git a/datos.json b/datos.json deleted file mode 100644 index eb0033fcf37d7591de5ad6742035c890203e8c84..0000000000000000000000000000000000000000 --- a/datos.json +++ /dev/null @@ -1,1226 +0,0 @@ -{ - "L0_suave_s1": { - "img": "imgs/L0_suave_s1.png", - "deriva_v1": 0.6786, - "deriva_circ": 2.4737257953593404e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 6, - 7, - 0.882 - ], - "par_despues": [ - 4, - 10, - 0.23 - ], - "mismo_par": false - }, - "L0_suave_s2": { - "img": "imgs/L0_suave_s2.png", - "deriva_v1": 0.7727, - "deriva_circ": 4.9862848498447405e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 6, - 7, - 0.882 - ], - "par_despues": [ - 0, - 7, - 0.23 - ], - "mismo_par": false - }, - "L0_media_s1": { - "img": "imgs/L0_media_s1.png", - "deriva_v1": 0.6294, - "deriva_circ": 1.552756834466925e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 6, - 7, - 0.882 - ], - "par_despues": [ - 5, - 6, - 0.21 - ], - "mismo_par": false - }, - "L0_media_s2": { - "img": "imgs/L0_media_s2.png", - "deriva_v1": 0.7734, - "deriva_circ": 6.469941050785416e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 6, - 7, - 0.882 - ], - "par_despues": [ - 1, - 6, - 0.217 - ], - "mismo_par": false - }, - "L0_fuerte_s1": { - "img": "imgs/L0_fuerte_s1.png", - "deriva_v1": 0.6268, - "deriva_circ": 2.6460257025054178e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 6, - 7, - 0.882 - ], - "par_despues": [ - 5, - 6, - 0.16 - ], - "mismo_par": false - }, - "L0_fuerte_s2": { - "img": "imgs/L0_fuerte_s2.png", - "deriva_v1": 0.787, - "deriva_circ": 2.133179670784439e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 6, - 7, - 0.882 - ], - "par_despues": [ - 1, - 6, - 0.251 - ], - "mismo_par": false - }, - "L1_suave_s1": { - "img": "imgs/L1_suave_s1.png", - "deriva_v1": 0.7452, - "deriva_circ": 2.99425601847311e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 2, - 8, - 0.548 - ], - "par_despues": [ - 1, - 8, - 0.172 - ], - "mismo_par": false - }, - "L1_suave_s2": { - "img": "imgs/L1_suave_s2.png", - "deriva_v1": 0.8741, - "deriva_circ": 5.601183727364095e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 2, - 8, - 0.548 - ], - "par_despues": [ - 2, - 8, - 0.116 - ], - "mismo_par": true - }, - "L1_media_s1": { - "img": "imgs/L1_media_s1.png", - "deriva_v1": 0.8091, - "deriva_circ": 1.80701113271146e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 2, - 8, - 0.548 - ], - "par_despues": [ - 4, - 5, - 0.11 - ], - "mismo_par": false - }, - "L1_media_s2": { - "img": "imgs/L1_media_s2.png", - "deriva_v1": 0.7987, - "deriva_circ": 7.415678744979554e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 2, - 8, - 0.548 - ], - "par_despues": [ - 5, - 11, - 0.154 - ], - "mismo_par": false - }, - "L1_fuerte_s1": { - "img": "imgs/L1_fuerte_s1.png", - "deriva_v1": 0.8351, - "deriva_circ": 2.963954738895629e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 2, - 8, - 0.548 - ], - "par_despues": [ - 5, - 7, - 0.122 - ], - "mismo_par": false - }, - "L1_fuerte_s2": { - "img": "imgs/L1_fuerte_s2.png", - "deriva_v1": 0.7861, - "deriva_circ": 2.613230767485379e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 2, - 8, - 0.548 - ], - "par_despues": [ - 5, - 11, - 0.148 - ], - "mismo_par": false - }, - "L2_suave_s1": { - "img": "imgs/L2_suave_s1.png", - "deriva_v1": 0.8127, - "deriva_circ": 2.879997179974982e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 2, - 11, - 0.512 - ], - "par_despues": [ - 2, - 8, - 0.118 - ], - "mismo_par": false - }, - "L2_suave_s2": { - "img": "imgs/L2_suave_s2.png", - "deriva_v1": 0.8315, - "deriva_circ": 5.764363813108119e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 2, - 11, - 0.512 - ], - "par_despues": [ - 2, - 11, - 0.158 - ], - "mismo_par": true - }, - "L2_media_s1": { - "img": "imgs/L2_media_s1.png", - "deriva_v1": 0.8766, - "deriva_circ": 1.905719006806521e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 2, - 11, - 0.512 - ], - "par_despues": [ - 2, - 11, - 0.193 - ], - "mismo_par": true - }, - "L2_media_s2": { - "img": "imgs/L2_media_s2.png", - "deriva_v1": 0.8431, - "deriva_circ": 8.191468928587784e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 2, - 11, - 0.512 - ], - "par_despues": [ - 1, - 6, - 0.144 - ], - "mismo_par": false - }, - "L2_fuerte_s1": { - "img": "imgs/L2_fuerte_s1.png", - "deriva_v1": 0.9084, - "deriva_circ": 3.027170995207917e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 2, - 11, - 0.512 - ], - "par_despues": [ - 2, - 11, - 0.19 - ], - "mismo_par": true - }, - "L2_fuerte_s2": { - "img": "imgs/L2_fuerte_s2.png", - "deriva_v1": 0.8408, - "deriva_circ": 2.6019565593414515e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 2, - 11, - 0.512 - ], - "par_despues": [ - 1, - 6, - 0.143 - ], - "mismo_par": false - }, - "L3_suave_s1": { - "img": "imgs/L3_suave_s1.png", - "deriva_v1": 0.8759, - "deriva_circ": 2.8129642052878183e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 4, - 9, - 0.171 - ], - "par_despues": [ - 2, - 9, - 0.113 - ], - "mismo_par": false - }, - "L3_suave_s2": { - "img": "imgs/L3_suave_s2.png", - "deriva_v1": 0.8475, - "deriva_circ": 5.809119070152899e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 4, - 9, - 0.171 - ], - "par_despues": [ - 3, - 8, - 0.11 - ], - "mismo_par": false - }, - "L3_media_s1": { - "img": "imgs/L3_media_s1.png", - "deriva_v1": 0.8642, - "deriva_circ": 1.8837829840199678e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 4, - 9, - 0.171 - ], - "par_despues": [ - 0, - 1, - 0.153 - ], - "mismo_par": false - }, - "L3_media_s2": { - "img": "imgs/L3_media_s2.png", - "deriva_v1": 0.8581, - "deriva_circ": 7.957037835563568e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 4, - 9, - 0.171 - ], - "par_despues": [ - 4, - 11, - 0.124 - ], - "mismo_par": false - }, - "L3_fuerte_s1": { - "img": "imgs/L3_fuerte_s1.png", - "deriva_v1": 0.8548, - "deriva_circ": 2.95812812652673e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 4, - 9, - 0.171 - ], - "par_despues": [ - 2, - 6, - 0.14 - ], - "mismo_par": false - }, - "L3_fuerte_s2": { - "img": "imgs/L3_fuerte_s2.png", - "deriva_v1": 0.8444, - "deriva_circ": 2.7124494874277234e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 4, - 9, - 0.171 - ], - "par_despues": [ - 4, - 11, - 0.112 - ], - "mismo_par": false - }, - "L4_suave_s1": { - "img": "imgs/L4_suave_s1.png", - "deriva_v1": 0.8383, - "deriva_circ": 2.9134602291283086e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 2, - 6, - 0.225 - ], - "par_despues": [ - 5, - 10, - 0.155 - ], - "mismo_par": false - }, - "L4_suave_s2": { - "img": "imgs/L4_suave_s2.png", - "deriva_v1": 0.8496, - "deriva_circ": 5.825031497296666e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 2, - 6, - 0.225 - ], - "par_despues": [ - 1, - 10, - 0.117 - ], - "mismo_par": false - }, - "L4_media_s1": { - "img": "imgs/L4_media_s1.png", - "deriva_v1": 0.8302, - "deriva_circ": 1.9144603416035137e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 2, - 6, - 0.225 - ], - "par_despues": [ - 5, - 10, - 0.16 - ], - "mismo_par": false - }, - "L4_media_s2": { - "img": "imgs/L4_media_s2.png", - "deriva_v1": 0.809, - "deriva_circ": 7.769447875944547e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 2, - 6, - 0.225 - ], - "par_despues": [ - 3, - 10, - 0.098 - ], - "mismo_par": false - }, - "L4_fuerte_s1": { - "img": "imgs/L4_fuerte_s1.png", - "deriva_v1": 0.8443, - "deriva_circ": 3.015301548006439e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 2, - 6, - 0.225 - ], - "par_despues": [ - 5, - 10, - 0.15 - ], - "mismo_par": false - }, - "L4_fuerte_s2": { - "img": "imgs/L4_fuerte_s2.png", - "deriva_v1": 0.8071, - "deriva_circ": 2.7054490186557386e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 2, - 6, - 0.225 - ], - "par_despues": [ - 1, - 9, - 0.113 - ], - "mismo_par": false - }, - "L5_suave_s1": { - "img": "imgs/L5_suave_s1.png", - "deriva_v1": 0.8377, - "deriva_circ": 3.047820941731155e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 3, - 9, - 0.454 - ], - "par_despues": [ - 5, - 7, - 0.189 - ], - "mismo_par": false - }, - "L5_suave_s2": { - "img": "imgs/L5_suave_s2.png", - "deriva_v1": 0.8498, - "deriva_circ": 5.865610656173236e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 3, - 9, - 0.454 - ], - "par_despues": [ - 3, - 9, - 0.103 - ], - "mismo_par": true - }, - "L5_media_s1": { - "img": "imgs/L5_media_s1.png", - "deriva_v1": 0.8772, - "deriva_circ": 1.931950503998985e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 3, - 9, - 0.454 - ], - "par_despues": [ - 1, - 9, - 0.135 - ], - "mismo_par": false - }, - "L5_media_s2": { - "img": "imgs/L5_media_s2.png", - "deriva_v1": 0.8122, - "deriva_circ": 8.236064575967118e-14, - "ordenes_magnitud": 12, - "par_antes": [ - 3, - 9, - 0.454 - ], - "par_despues": [ - 0, - 1, - 0.098 - ], - "mismo_par": false - }, - "L5_fuerte_s1": { - "img": "imgs/L5_fuerte_s1.png", - "deriva_v1": 0.8701, - "deriva_circ": 3.0786391199404436e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 3, - 9, - 0.454 - ], - "par_despues": [ - 1, - 9, - 0.121 - ], - "mismo_par": false - }, - "L5_fuerte_s2": { - "img": "imgs/L5_fuerte_s2.png", - "deriva_v1": 0.7986, - "deriva_circ": 2.7028471609653922e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 3, - 9, - 0.454 - ], - "par_despues": [ - 0, - 1, - 0.106 - ], - "mismo_par": false - }, - "L6_suave_s1": { - "img": "imgs/L6_suave_s1.png", - "deriva_v1": 0.8506, - "deriva_circ": 3.162705240413337e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 6, - 8, - 0.703 - ], - "par_despues": [ - 5, - 9, - 0.13 - ], - "mismo_par": false - }, - "L6_suave_s2": { - "img": "imgs/L6_suave_s2.png", - "deriva_v1": 0.8241, - "deriva_circ": 5.797686128711883e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 6, - 8, - 0.703 - ], - "par_despues": [ - 10, - 11, - 0.208 - ], - "mismo_par": false - }, - "L6_media_s1": { - "img": "imgs/L6_media_s1.png", - "deriva_v1": 0.8474, - "deriva_circ": 1.9306993491334054e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 6, - 8, - 0.703 - ], - "par_despues": [ - 2, - 6, - 0.139 - ], - "mismo_par": false - }, - "L6_media_s2": { - "img": "imgs/L6_media_s2.png", - "deriva_v1": 0.8537, - "deriva_circ": 7.479571097365316e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 6, - 8, - 0.703 - ], - "par_despues": [ - 2, - 6, - 0.132 - ], - "mismo_par": false - }, - "L6_fuerte_s1": { - "img": "imgs/L6_fuerte_s1.png", - "deriva_v1": 0.8259, - "deriva_circ": 2.9529236640449954e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 6, - 8, - 0.703 - ], - "par_despues": [ - 2, - 6, - 0.17 - ], - "mismo_par": false - }, - "L6_fuerte_s2": { - "img": "imgs/L6_fuerte_s2.png", - "deriva_v1": 0.8447, - "deriva_circ": 2.7626367797404217e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 6, - 8, - 0.703 - ], - "par_despues": [ - 2, - 6, - 0.113 - ], - "mismo_par": false - }, - "L7_suave_s1": { - "img": "imgs/L7_suave_s1.png", - "deriva_v1": 0.8616, - "deriva_circ": 3.1011565115169496e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 5, - 10, - 0.647 - ], - "par_despues": [ - 9, - 10, - 0.101 - ], - "mismo_par": false - }, - "L7_suave_s2": { - "img": "imgs/L7_suave_s2.png", - "deriva_v1": 0.8613, - "deriva_circ": 5.761667178513975e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 5, - 10, - 0.647 - ], - "par_despues": [ - 3, - 5, - 0.112 - ], - "mismo_par": false - }, - "L7_media_s1": { - "img": "imgs/L7_media_s1.png", - "deriva_v1": 0.8736, - "deriva_circ": 1.9817014947860575e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 5, - 10, - 0.647 - ], - "par_despues": [ - 5, - 8, - 0.127 - ], - "mismo_par": false - }, - "L7_media_s2": { - "img": "imgs/L7_media_s2.png", - "deriva_v1": 0.8387, - "deriva_circ": 8.280716335893669e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 5, - 10, - 0.647 - ], - "par_despues": [ - 1, - 7, - 0.088 - ], - "mismo_par": false - }, - "L7_fuerte_s1": { - "img": "imgs/L7_fuerte_s1.png", - "deriva_v1": 0.8789, - "deriva_circ": 3.1076818388437473e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 5, - 10, - 0.647 - ], - "par_despues": [ - 5, - 8, - 0.113 - ], - "mismo_par": false - }, - "L7_fuerte_s2": { - "img": "imgs/L7_fuerte_s2.png", - "deriva_v1": 0.8362, - "deriva_circ": 2.794673517007365e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 5, - 10, - 0.647 - ], - "par_despues": [ - 8, - 10, - 0.07 - ], - "mismo_par": false - }, - "L8_suave_s1": { - "img": "imgs/L8_suave_s1.png", - "deriva_v1": 0.8371, - "deriva_circ": 3.159660707976963e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 6, - 8, - 0.923 - ], - "par_despues": [ - 3, - 6, - 0.106 - ], - "mismo_par": false - }, - "L8_suave_s2": { - "img": "imgs/L8_suave_s2.png", - "deriva_v1": 0.8326, - "deriva_circ": 6.179127019580005e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 6, - 8, - 0.923 - ], - "par_despues": [ - 5, - 6, - 0.087 - ], - "mismo_par": false - }, - "L8_media_s1": { - "img": "imgs/L8_media_s1.png", - "deriva_v1": 0.8297, - "deriva_circ": 1.9967340691735304e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 6, - 8, - 0.923 - ], - "par_despues": [ - 6, - 8, - 0.119 - ], - "mismo_par": true - }, - "L8_media_s2": { - "img": "imgs/L8_media_s2.png", - "deriva_v1": 0.8221, - "deriva_circ": 8.406723915372021e-14, - "ordenes_magnitud": 12, - "par_antes": [ - 6, - 8, - 0.923 - ], - "par_despues": [ - 3, - 7, - 0.091 - ], - "mismo_par": false - }, - "L8_fuerte_s1": { - "img": "imgs/L8_fuerte_s1.png", - "deriva_v1": 0.844, - "deriva_circ": 3.1070542356021955e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 6, - 8, - 0.923 - ], - "par_despues": [ - 2, - 8, - 0.113 - ], - "mismo_par": false - }, - "L8_fuerte_s2": { - "img": "imgs/L8_fuerte_s2.png", - "deriva_v1": 0.827, - "deriva_circ": 2.8388147017913648e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 6, - 8, - 0.923 - ], - "par_despues": [ - 8, - 11, - 0.092 - ], - "mismo_par": false - }, - "L9_suave_s1": { - "img": "imgs/L9_suave_s1.png", - "deriva_v1": 0.91, - "deriva_circ": 3.113742104090125e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 0, - 8, - 0.422 - ], - "par_despues": [ - 0, - 8, - 0.109 - ], - "mismo_par": true - }, - "L9_suave_s2": { - "img": "imgs/L9_suave_s2.png", - "deriva_v1": 0.877, - "deriva_circ": 6.094876258725251e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 0, - 8, - 0.422 - ], - "par_despues": [ - 3, - 4, - 0.104 - ], - "mismo_par": false - }, - "L9_media_s1": { - "img": "imgs/L9_media_s1.png", - "deriva_v1": 0.933, - "deriva_circ": 2.007609235255005e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 0, - 8, - 0.422 - ], - "par_despues": [ - 3, - 5, - 0.074 - ], - "mismo_par": false - }, - "L9_media_s2": { - "img": "imgs/L9_media_s2.png", - "deriva_v1": 0.8425, - "deriva_circ": 8.324532178188378e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 0, - 8, - 0.422 - ], - "par_despues": [ - 3, - 10, - 0.121 - ], - "mismo_par": false - }, - "L9_fuerte_s1": { - "img": "imgs/L9_fuerte_s1.png", - "deriva_v1": 0.9228, - "deriva_circ": 3.17377462724315e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 0, - 8, - 0.422 - ], - "par_despues": [ - 5, - 9, - 0.081 - ], - "mismo_par": false - }, - "L9_fuerte_s2": { - "img": "imgs/L9_fuerte_s2.png", - "deriva_v1": 0.8389, - "deriva_circ": 2.8617318086734605e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 0, - 8, - 0.422 - ], - "par_despues": [ - 3, - 10, - 0.109 - ], - "mismo_par": false - }, - "L10_suave_s1": { - "img": "imgs/L10_suave_s1.png", - "deriva_v1": 0.8477, - "deriva_circ": 3.110335961284467e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 0, - 3, - 0.583 - ], - "par_despues": [ - 7, - 10, - 0.105 - ], - "mismo_par": false - }, - "L10_suave_s2": { - "img": "imgs/L10_suave_s2.png", - "deriva_v1": 0.8731, - "deriva_circ": 6.105995368288468e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 0, - 3, - 0.583 - ], - "par_despues": [ - 0, - 7, - 0.097 - ], - "mismo_par": false - }, - "L10_media_s1": { - "img": "imgs/L10_media_s1.png", - "deriva_v1": 0.8662, - "deriva_circ": 1.9916351714317566e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 0, - 3, - 0.583 - ], - "par_despues": [ - 7, - 8, - 0.108 - ], - "mismo_par": false - }, - "L10_media_s2": { - "img": "imgs/L10_media_s2.png", - "deriva_v1": 0.8814, - "deriva_circ": 8.313739099757465e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 0, - 3, - 0.583 - ], - "par_despues": [ - 0, - 7, - 0.088 - ], - "mismo_par": false - }, - "L10_fuerte_s1": { - "img": "imgs/L10_fuerte_s1.png", - "deriva_v1": 0.8551, - "deriva_circ": 3.12802461650481e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 0, - 3, - 0.583 - ], - "par_despues": [ - 7, - 8, - 0.095 - ], - "mismo_par": false - }, - "L10_fuerte_s2": { - "img": "imgs/L10_fuerte_s2.png", - "deriva_v1": 0.8863, - "deriva_circ": 2.869690316839748e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 0, - 3, - 0.583 - ], - "par_despues": [ - 1, - 10, - 0.084 - ], - "mismo_par": false - }, - "L11_suave_s1": { - "img": "imgs/L11_suave_s1.png", - "deriva_v1": 0.7919, - "deriva_circ": 2.878703791209282e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 8, - 9, - 0.92 - ], - "par_despues": [ - 3, - 7, - 0.176 - ], - "mismo_par": false - }, - "L11_suave_s2": { - "img": "imgs/L11_suave_s2.png", - "deriva_v1": 0.8022, - "deriva_circ": 6.204952614598513e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 8, - 9, - 0.92 - ], - "par_despues": [ - 4, - 9, - 0.237 - ], - "mismo_par": false - }, - "L11_media_s1": { - "img": "imgs/L11_media_s1.png", - "deriva_v1": 0.7892, - "deriva_circ": 1.984885674138256e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 8, - 9, - 0.92 - ], - "par_despues": [ - 3, - 7, - 0.185 - ], - "mismo_par": false - }, - "L11_media_s2": { - "img": "imgs/L11_media_s2.png", - "deriva_v1": 0.8032, - "deriva_circ": 8.129645132437228e-14, - "ordenes_magnitud": 12, - "par_antes": [ - 8, - 9, - 0.92 - ], - "par_despues": [ - 4, - 9, - 0.22 - ], - "mismo_par": false - }, - "L11_fuerte_s1": { - "img": "imgs/L11_fuerte_s1.png", - "deriva_v1": 0.7973, - "deriva_circ": 3.1385631552222743e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 8, - 9, - 0.92 - ], - "par_despues": [ - 3, - 7, - 0.181 - ], - "mismo_par": false - }, - "L11_fuerte_s2": { - "img": "imgs/L11_fuerte_s2.png", - "deriva_v1": 0.8202, - "deriva_circ": 2.807503880189621e-14, - "ordenes_magnitud": 13, - "par_antes": [ - 8, - 9, - 0.92 - ], - "par_despues": [ - 4, - 9, - 0.178 - ], - "mismo_par": false - } -} \ No newline at end of file diff --git a/demo/portador.py b/demo/portador.py new file mode 100644 index 0000000000000000000000000000000000000000..3e3668229bab00f57623d7e15f976a52643aae4a --- /dev/null +++ b/demo/portador.py @@ -0,0 +1,227 @@ +"""portador ligero del sector valor-salida para la demo. + +`src/gauge_flip.py` opera sobre `modelo.blocks[capa].attn.qkv` y +`.proj`, es decir sobre un vit de timm de 984 mb. la demo no puede +cargar eso, pero tampoco debe reimplementar el gauge: reimplementarlo +sería perder la propiedad que hace fiable a esta ruta ---que la demo +ejecuta el mismo código que produjo las tablas 2 y 3---. + +la salida es un objeto mínimo que expone exactamente esa interfaz +sobre los tensores del safetensors. `aplica_gauge_ov` corre **sin una +sola modificación**; solo toca las filas de valor del qkv, su sesgo y +las columnas de la proyección de salida, que es lo que el portador +tiene. el resto del qkv es relleno que la función jamás lee. +""" + +import pathlib + +import torch +from safetensors.torch import load_file + + +class _Lineal: + """imita `nn.Linear` en los dos atributos que el gauge usa.""" + + def __init__(self, weight: torch.Tensor, + bias: torch.Tensor | None = None) -> None: + """guarda peso y sesgo como tensores mutables. + + Args: + weight: matriz de pesos. + bias: vector de sesgo, o None. + """ + self.weight = weight + self.bias = bias + + +class _Atencion: + """imita el módulo de atención de timm: `.qkv` y `.proj`.""" + + def __init__(self, qkv: _Lineal, proj: _Lineal) -> None: + """agrupa las dos proyecciones. + + Args: + qkv: proyección fusionada consulta-clave-valor. + proj: proyección de salida. + """ + self.qkv = qkv + self.proj = proj + + +class _Bloque: + """imita un bloque de timm: solo expone `.attn`.""" + + def __init__(self, attn: _Atencion) -> None: + """envuelve la atención. + + Args: + attn: el módulo de atención del bloque. + """ + self.attn = attn + + +class PortadorVO: + """vit de mentira con el sector valor-salida de verdad. + + Attributes: + blocks: lista de bloques con la interfaz que el gauge espera. + n_cabezas: cabezas por capa. + dim_cabeza: d_h. + """ + + def __init__(self, ruta: str) -> None: + """monta el portador desde el safetensors del sector. + + los tensores se elevan a float64. `aplica_gauge_ov` calcula r y + su inversa en doble y devuelve el resultado al dtype del peso: + con el portador en simple, ese último casteo deja un error de + ~1e-7 relativo y el circuito ov ---que es exactamente + invariante--- aparece moviéndose milésimas de grado, más que + v1(w_o) bajo un gauge ortogonal. en doble no se pierde nada y + la invariancia se lee al orden que certifica la fase g. + + Args: + ruta: fichero generado por `scripts/extraer_sector_vo.py`. + """ + s = load_file(ruta) + capas = 1 + max(int(k.split(".")[0][1:]) for k in s) + self.n_cabezas = 1 + max(int(k.split(".")[1][1:]) for k in s) + self.dim_cabeza = s["L0.h0.w_v"].shape[0] + d = s["L0.h0.w_v"].shape[1] + nh, dh = self.n_cabezas, self.dim_cabeza + self.blocks = [] + for c in range(capas): + qkv_w = torch.zeros(3 * d, d, dtype=torch.float64) + qkv_b = torch.zeros(3 * d, dtype=torch.float64) + proj_w = torch.zeros(d, d, dtype=torch.float64) + for h in range(nh): + fil = slice(2 * d + h * dh, 2 * d + (h + 1) * dh) + col = slice(h * dh, (h + 1) * dh) + qkv_w[fil, :] = s[f"L{c}.h{h}.w_v"].double() + qkv_b[fil] = s[f"L{c}.h{h}.b_v"].double() + proj_w[:, col] = ( + s[f"L{c}.h{h}.w_o"].t().double()) + self.blocks.append( + _Bloque(_Atencion(_Lineal(qkv_w, qkv_b), _Lineal(proj_w)))) + + def w_o_por_cabeza(self, capa: int) -> torch.Tensor: + """proyección de salida por cabeza, tras el gauge que haya. + + Args: + capa: índice de capa. + + Returns: + tensor [h, dh, d]. + """ + w = self.blocks[capa].attn.proj.weight + dh = self.dim_cabeza + return torch.stack([w[:, h * dh:(h + 1) * dh].t() + for h in range(self.n_cabezas)]) + + def copia(self) -> "PortadorVO": + """duplica el portador para aplicarle un gauge sin destruirlo. + + Returns: + un portador independiente con los mismos pesos. + """ + otro = PortadorVO.__new__(PortadorVO) + otro.n_cabezas, otro.dim_cabeza = self.n_cabezas, self.dim_cabeza + otro.blocks = [] + for b in self.blocks: + q, p = b.attn.qkv, b.attn.proj + otro.blocks.append(_Bloque(_Atencion( + _Lineal(q.weight.clone(), q.bias.clone()), + _Lineal(p.weight.clone())))) + return otro + + +def carga(col: str = "vitb", raiz: str = "artifacts/demo") -> PortadorVO: + """carga el portador de una columna. + + Args: + col: etiqueta de columna (vitb o pythia). + raiz: directorio de los safetensors. + + Returns: + el portador montado. + + Raises: + FileNotFoundError: si falta el fichero del sector. + """ + ruta = pathlib.Path(raiz) / f"sector_vo_{col}.safetensors" + if not ruta.exists(): + raise FileNotFoundError( + f"{ruta}: ejecuta antes scripts/extraer_sector_vo.py") + return PortadorVO(str(ruta)) + + +def verifica_relleno(p: PortadorVO, capa: int = 0, + escala_id: float = 8.0) -> None: + """comprueba que el gauge no toca las regiones de relleno. + + el portador funciona porque `aplica_gauge_ov` solo lee y escribe + las filas de valor del qkv, su sesgo y las columnas de la + proyección de salida. eso es un contrato implícito con `src/`: si + una versión futura del gauge tocara consulta o clave, el portador + serviría derivas de un modelo que ya no es el del paper, y lo + haría en silencio. este assert convierte el contrato en fallo + ruidoso. + + Args: + p: portador ya montado. + capa: capa sobre la que probar. + escala_id: fuerza del gauge de prueba. + + Raises: + AssertionError: si el gauge escribe fuera del sector de valor. + """ + from src.gauge_flip import aplica_gauge_ov + + q = p.copia() + w = q.blocks[capa].attn.qkv.weight + d = w.shape[1] + antes_w = w[:2 * d, :].clone() + antes_b = q.blocks[capa].attn.qkv.bias[:2 * d].clone() + aplica_gauge_ov(q, capa, q.n_cabezas, q.dim_cabeza, semilla=0, + escala_id=escala_id) + assert torch.equal(q.blocks[capa].attn.qkv.weight[:2 * d, :], antes_w), ( + "el gauge escribió en las filas de consulta o clave: el portador " + "ya no representa al modelo del paper") + assert torch.equal(q.blocks[capa].attn.qkv.bias[:2 * d], antes_b), ( + "el gauge escribió en el sesgo de consulta o clave") + + +def verifica_manifiesto(raiz: str = ".") -> None: + """contrasta el sha256 de lo vendido contra `manifiesto.sha256`. + + el Space lleva copias del código del paper, y las copias derivan. + esto es el patrón de identidad byte a byte de g4 aplicado al + código: si un fichero cambió, el arranque falla en vez de servir + derivas de algo que ya no es el aparato publicado. si no hay + manifiesto ---ejecución local desde el repo--- no hay nada que + contrastar y la función no hace nada. + + Args: + raiz: directorio donde vive `manifiesto.sha256`. + + Raises: + AssertionError: si algún fichero falta o su hash no cuadra. + """ + import hashlib + + m = pathlib.Path(raiz) / "manifiesto.sha256" + if not m.exists(): + return + for linea in m.read_text(encoding="utf-8").splitlines(): + if not linea.strip(): + continue + esperado, rel = linea.split(" ", 1) + f = pathlib.Path(raiz) / rel + assert f.exists(), f"manifiesto: falta {rel}" + h = hashlib.sha256() + with open(f, "rb") as fh: + for b in iter(lambda: fh.read(1 << 20), b""): + h.update(b) + assert h.hexdigest() == esperado, ( + f"manifiesto: {rel} ha derivado respecto al commit firmado; " + f"el Space no sirve un código que no es el del paper") diff --git a/imgs/L0_fuerte_s1.png b/imgs/L0_fuerte_s1.png deleted file mode 100644 index 53604411640204c4222b26de0214249b4bb790f4..0000000000000000000000000000000000000000 Binary files a/imgs/L0_fuerte_s1.png and /dev/null differ diff --git a/imgs/L0_fuerte_s2.png b/imgs/L0_fuerte_s2.png deleted file mode 100644 index b42f3eb2f1cbe46b4da31e81b5f32b18fa5ab9b3..0000000000000000000000000000000000000000 Binary files a/imgs/L0_fuerte_s2.png and /dev/null differ diff --git a/imgs/L0_media_s1.png b/imgs/L0_media_s1.png deleted file mode 100644 index 8fd0cbc0295cfd876c8411aad52bf7a25704c3f0..0000000000000000000000000000000000000000 Binary files a/imgs/L0_media_s1.png and /dev/null differ diff --git a/imgs/L0_media_s2.png b/imgs/L0_media_s2.png deleted file mode 100644 index f8ffcb16fa2c9bc619b03daccbc61091be322825..0000000000000000000000000000000000000000 Binary files a/imgs/L0_media_s2.png and /dev/null differ diff --git a/imgs/L0_suave_s1.png b/imgs/L0_suave_s1.png deleted file mode 100644 index a50dc2fbc871864428529d9741c7e5a09b91ad45..0000000000000000000000000000000000000000 Binary files a/imgs/L0_suave_s1.png and /dev/null differ diff --git a/imgs/L0_suave_s2.png b/imgs/L0_suave_s2.png deleted file mode 100644 index 10a3a0022dbf4f38be572660299890c44674144a..0000000000000000000000000000000000000000 Binary files a/imgs/L0_suave_s2.png and /dev/null differ diff --git a/imgs/L10_fuerte_s1.png b/imgs/L10_fuerte_s1.png deleted file mode 100644 index b3a6acab8e4fe2ce6101c73f498df70dbf2c3309..0000000000000000000000000000000000000000 Binary files a/imgs/L10_fuerte_s1.png and /dev/null differ diff --git a/imgs/L10_fuerte_s2.png b/imgs/L10_fuerte_s2.png deleted file mode 100644 index 193a072764fe01329ac2659b497569fb49f2e40a..0000000000000000000000000000000000000000 Binary files a/imgs/L10_fuerte_s2.png and /dev/null differ diff --git a/imgs/L10_media_s1.png b/imgs/L10_media_s1.png deleted file mode 100644 index 9bf7b67fb5bfa7f397f9917f1398cdb9604a2707..0000000000000000000000000000000000000000 Binary files a/imgs/L10_media_s1.png and /dev/null differ diff --git a/imgs/L10_media_s2.png b/imgs/L10_media_s2.png deleted file mode 100644 index a426ac61a135ea2a9c2af682ab2c23ba2b2b701b..0000000000000000000000000000000000000000 Binary files a/imgs/L10_media_s2.png and /dev/null differ diff --git a/imgs/L10_suave_s1.png b/imgs/L10_suave_s1.png deleted file mode 100644 index 51773326300d32ac5a6e59cf0cd8ee830c17cf42..0000000000000000000000000000000000000000 Binary files a/imgs/L10_suave_s1.png and /dev/null differ diff --git a/imgs/L10_suave_s2.png b/imgs/L10_suave_s2.png deleted file mode 100644 index 12de3db9ad5fc9295e95d2c97882be2c0117b9ea..0000000000000000000000000000000000000000 Binary files a/imgs/L10_suave_s2.png and /dev/null differ diff --git a/imgs/L11_fuerte_s1.png b/imgs/L11_fuerte_s1.png deleted file mode 100644 index fa717f7ef1339d78dbbae9cd74aa847cf2a814ad..0000000000000000000000000000000000000000 Binary files a/imgs/L11_fuerte_s1.png and /dev/null differ diff --git a/imgs/L11_fuerte_s2.png b/imgs/L11_fuerte_s2.png deleted file mode 100644 index f6b304c94b35f836b456f5c28207243c42607017..0000000000000000000000000000000000000000 Binary files a/imgs/L11_fuerte_s2.png and /dev/null differ diff --git a/imgs/L11_media_s1.png b/imgs/L11_media_s1.png deleted file mode 100644 index bd3f58683e641bef123321c4400239c26ff50fe0..0000000000000000000000000000000000000000 Binary files a/imgs/L11_media_s1.png and /dev/null differ diff --git a/imgs/L11_media_s2.png b/imgs/L11_media_s2.png deleted file mode 100644 index a872fa4eeef8da785a4d362bec47a161a9c170d4..0000000000000000000000000000000000000000 Binary files a/imgs/L11_media_s2.png and /dev/null differ diff --git a/imgs/L11_suave_s1.png b/imgs/L11_suave_s1.png deleted file mode 100644 index be7a67eb5e6f32a5ce8f524a51c28993cb8a61d6..0000000000000000000000000000000000000000 Binary files a/imgs/L11_suave_s1.png and /dev/null differ diff --git a/imgs/L11_suave_s2.png b/imgs/L11_suave_s2.png deleted file mode 100644 index 67fce007e43e139aa0f99cea5fa2eb3560108c35..0000000000000000000000000000000000000000 Binary files a/imgs/L11_suave_s2.png and /dev/null differ diff --git a/imgs/L1_fuerte_s1.png b/imgs/L1_fuerte_s1.png deleted file mode 100644 index 0d50b6233a7715c8fdfd1f99b736fda33c232154..0000000000000000000000000000000000000000 Binary files a/imgs/L1_fuerte_s1.png and /dev/null differ diff --git a/imgs/L1_fuerte_s2.png b/imgs/L1_fuerte_s2.png deleted file mode 100644 index f14c37d96ddf5d39a6c00a813e8ec788df482238..0000000000000000000000000000000000000000 Binary files a/imgs/L1_fuerte_s2.png and /dev/null differ diff --git a/imgs/L1_media_s1.png b/imgs/L1_media_s1.png deleted file mode 100644 index f89e96a12d75957aef21da63a390f8288517c9f9..0000000000000000000000000000000000000000 Binary files a/imgs/L1_media_s1.png and /dev/null differ diff --git a/imgs/L1_media_s2.png b/imgs/L1_media_s2.png deleted file mode 100644 index 6aa182e4d80fd061d15236741c20cf96cd0307f9..0000000000000000000000000000000000000000 Binary files a/imgs/L1_media_s2.png and /dev/null differ diff --git a/imgs/L1_suave_s1.png b/imgs/L1_suave_s1.png deleted file mode 100644 index 4bee98bf225ae729445a505d5840a13334036d5a..0000000000000000000000000000000000000000 Binary files a/imgs/L1_suave_s1.png and /dev/null differ diff --git a/imgs/L1_suave_s2.png b/imgs/L1_suave_s2.png deleted file mode 100644 index 449bbde11ea882ac29cc77e55c7e180252185087..0000000000000000000000000000000000000000 Binary files a/imgs/L1_suave_s2.png and /dev/null differ diff --git a/imgs/L2_fuerte_s1.png b/imgs/L2_fuerte_s1.png deleted file mode 100644 index 736b2d93f0cea4aa06803c145dd626c0ea0ba090..0000000000000000000000000000000000000000 Binary files a/imgs/L2_fuerte_s1.png and /dev/null differ diff --git a/imgs/L2_fuerte_s2.png b/imgs/L2_fuerte_s2.png deleted file mode 100644 index 32509c35499df850c487ca3ddb6f4ef4c1231ed8..0000000000000000000000000000000000000000 Binary files a/imgs/L2_fuerte_s2.png and /dev/null differ diff --git a/imgs/L2_media_s1.png b/imgs/L2_media_s1.png deleted file mode 100644 index 0283bc2da9d4a6661a73b9efb60252eadbf9d846..0000000000000000000000000000000000000000 Binary files a/imgs/L2_media_s1.png and /dev/null differ diff --git a/imgs/L2_media_s2.png b/imgs/L2_media_s2.png deleted file mode 100644 index db815d3400f45b2af3d42ef01e83d94a4ae7ce78..0000000000000000000000000000000000000000 Binary files a/imgs/L2_media_s2.png and /dev/null differ diff --git a/imgs/L2_suave_s1.png b/imgs/L2_suave_s1.png deleted file mode 100644 index 9681f7d5df675b71b540780e5d8b8137a7cd6a4f..0000000000000000000000000000000000000000 Binary files a/imgs/L2_suave_s1.png and /dev/null differ diff --git a/imgs/L2_suave_s2.png b/imgs/L2_suave_s2.png deleted file mode 100644 index 8e0246374d12f4567cc2aff08448ec96da40a650..0000000000000000000000000000000000000000 Binary files a/imgs/L2_suave_s2.png and /dev/null differ diff --git a/imgs/L3_fuerte_s1.png b/imgs/L3_fuerte_s1.png deleted file mode 100644 index b1693a557e59f72a1ccd8b0ce34f93516862234b..0000000000000000000000000000000000000000 Binary files a/imgs/L3_fuerte_s1.png and /dev/null differ diff --git a/imgs/L3_fuerte_s2.png b/imgs/L3_fuerte_s2.png deleted file mode 100644 index 397df15dd2b126f31d9900b785196cdc10f8718c..0000000000000000000000000000000000000000 Binary files a/imgs/L3_fuerte_s2.png and /dev/null differ diff --git a/imgs/L3_media_s1.png b/imgs/L3_media_s1.png deleted file mode 100644 index 3095c699258c31f5658e03b0db8aef687c911ac4..0000000000000000000000000000000000000000 Binary files a/imgs/L3_media_s1.png and /dev/null differ diff --git a/imgs/L3_media_s2.png b/imgs/L3_media_s2.png deleted file mode 100644 index 9a30ade59e2ebdc69e699c64e2c26d5f491a89f1..0000000000000000000000000000000000000000 Binary files a/imgs/L3_media_s2.png and /dev/null differ diff --git a/imgs/L3_suave_s1.png b/imgs/L3_suave_s1.png deleted file mode 100644 index 007b78d482f96c1e4e53d0d08d758030a4e38a31..0000000000000000000000000000000000000000 Binary files a/imgs/L3_suave_s1.png and /dev/null differ diff --git a/imgs/L3_suave_s2.png b/imgs/L3_suave_s2.png deleted file mode 100644 index e04f5684b473007c74eadf11afda83c2f7337989..0000000000000000000000000000000000000000 Binary files a/imgs/L3_suave_s2.png and /dev/null differ diff --git a/imgs/L4_fuerte_s1.png b/imgs/L4_fuerte_s1.png deleted file mode 100644 index c35ae9d332aa0766dc7acf9ce18281b6b15e655c..0000000000000000000000000000000000000000 Binary files a/imgs/L4_fuerte_s1.png and /dev/null differ diff --git a/imgs/L4_fuerte_s2.png b/imgs/L4_fuerte_s2.png deleted file mode 100644 index 54a818953dac4a133e8678170db1b7a6c2138bfa..0000000000000000000000000000000000000000 Binary files a/imgs/L4_fuerte_s2.png and /dev/null differ diff --git a/imgs/L4_media_s1.png b/imgs/L4_media_s1.png deleted file mode 100644 index 2f6c94110e4e004345561e9cd9f46e0e65a2268e..0000000000000000000000000000000000000000 Binary files a/imgs/L4_media_s1.png and /dev/null differ diff --git a/imgs/L4_media_s2.png b/imgs/L4_media_s2.png deleted file mode 100644 index 32fe8fdcde897dc5f199e464be0ab6f357c0d80c..0000000000000000000000000000000000000000 Binary files a/imgs/L4_media_s2.png and /dev/null differ diff --git a/imgs/L4_suave_s1.png b/imgs/L4_suave_s1.png deleted file mode 100644 index ac0e4a0dbf88a8f4b615b34c2817d3a67c85c63b..0000000000000000000000000000000000000000 Binary files a/imgs/L4_suave_s1.png and /dev/null differ diff --git a/imgs/L4_suave_s2.png b/imgs/L4_suave_s2.png deleted file mode 100644 index 5c740c2e396f49068687ee0214c72ea7cd7009de..0000000000000000000000000000000000000000 Binary files a/imgs/L4_suave_s2.png and /dev/null differ diff --git a/imgs/L5_fuerte_s1.png b/imgs/L5_fuerte_s1.png deleted file mode 100644 index 96daf0cd30273170543dce16c01719d96d15b15e..0000000000000000000000000000000000000000 Binary files a/imgs/L5_fuerte_s1.png and /dev/null differ diff --git a/imgs/L5_fuerte_s2.png b/imgs/L5_fuerte_s2.png deleted file mode 100644 index f0465cf6369134ce33f97de2e27206bcb13cb94a..0000000000000000000000000000000000000000 Binary files a/imgs/L5_fuerte_s2.png and /dev/null differ diff --git a/imgs/L5_media_s1.png b/imgs/L5_media_s1.png deleted file mode 100644 index 03a6f223e5470ffdc1d12dc141011dbd129fd37c..0000000000000000000000000000000000000000 Binary files a/imgs/L5_media_s1.png and /dev/null differ diff --git a/imgs/L5_media_s2.png b/imgs/L5_media_s2.png deleted file mode 100644 index 718573d75088cff4f0e4fcb45690d136768bde7a..0000000000000000000000000000000000000000 Binary files a/imgs/L5_media_s2.png and /dev/null differ diff --git a/imgs/L5_suave_s1.png b/imgs/L5_suave_s1.png deleted file mode 100644 index ae8c5a7976390153b9af5299aafd6b28542cca15..0000000000000000000000000000000000000000 Binary files a/imgs/L5_suave_s1.png and /dev/null differ diff --git a/imgs/L5_suave_s2.png b/imgs/L5_suave_s2.png deleted file mode 100644 index 86826a08114eb3a03ca5876a95c29f5100936bab..0000000000000000000000000000000000000000 Binary files a/imgs/L5_suave_s2.png and /dev/null differ diff --git a/imgs/L6_fuerte_s1.png b/imgs/L6_fuerte_s1.png deleted file mode 100644 index 994674123b4eee6c02742156be57114518f9da2b..0000000000000000000000000000000000000000 Binary files a/imgs/L6_fuerte_s1.png and /dev/null differ diff --git a/imgs/L6_fuerte_s2.png b/imgs/L6_fuerte_s2.png deleted file mode 100644 index c7d2c75dbe7baf901088b5535facc681b9ccd7d3..0000000000000000000000000000000000000000 Binary files a/imgs/L6_fuerte_s2.png and /dev/null differ diff --git a/imgs/L6_media_s1.png b/imgs/L6_media_s1.png deleted file mode 100644 index a86243922801b9fb911077d80ae294e27a269276..0000000000000000000000000000000000000000 Binary files a/imgs/L6_media_s1.png and /dev/null differ diff --git a/imgs/L6_media_s2.png b/imgs/L6_media_s2.png deleted file mode 100644 index 885e2cf671e01b73f2b93d6058dff0a27d7134bb..0000000000000000000000000000000000000000 Binary files a/imgs/L6_media_s2.png and /dev/null differ diff --git a/imgs/L6_suave_s1.png b/imgs/L6_suave_s1.png deleted file mode 100644 index e69968afc6d1bf9ce9ec5cd6e02066c7737a48d8..0000000000000000000000000000000000000000 Binary files a/imgs/L6_suave_s1.png and /dev/null differ diff --git a/imgs/L6_suave_s2.png b/imgs/L6_suave_s2.png deleted file mode 100644 index 1cf3a7b8f2ff2c08d85385f8353b79e2cd36596f..0000000000000000000000000000000000000000 Binary files a/imgs/L6_suave_s2.png and /dev/null differ diff --git a/imgs/L7_fuerte_s1.png b/imgs/L7_fuerte_s1.png deleted file mode 100644 index f4f2a278e5f222c6e8e162501481945d5434e975..0000000000000000000000000000000000000000 Binary files a/imgs/L7_fuerte_s1.png and /dev/null differ diff --git a/imgs/L7_fuerte_s2.png b/imgs/L7_fuerte_s2.png deleted file mode 100644 index 88fd308a89df6ba4629195e26cfe7108da44d864..0000000000000000000000000000000000000000 Binary files a/imgs/L7_fuerte_s2.png and /dev/null differ diff --git a/imgs/L7_media_s1.png b/imgs/L7_media_s1.png deleted file mode 100644 index 2e635c982abddc85873fd783080f1842e294768d..0000000000000000000000000000000000000000 Binary files a/imgs/L7_media_s1.png and /dev/null differ diff --git a/imgs/L7_media_s2.png b/imgs/L7_media_s2.png deleted file mode 100644 index 994e575c49bbb09ae7393e431c528c62b068960f..0000000000000000000000000000000000000000 Binary files a/imgs/L7_media_s2.png and /dev/null differ diff --git a/imgs/L7_suave_s1.png b/imgs/L7_suave_s1.png deleted file mode 100644 index 54dfe8ba93e51ceb4dc5a87c7e3dc03c6d0c570a..0000000000000000000000000000000000000000 Binary files a/imgs/L7_suave_s1.png and /dev/null differ diff --git a/imgs/L7_suave_s2.png b/imgs/L7_suave_s2.png deleted file mode 100644 index 02e1c6162d783174366f4324c36d4e60d8c87e3e..0000000000000000000000000000000000000000 Binary files a/imgs/L7_suave_s2.png and /dev/null differ diff --git a/imgs/L8_fuerte_s1.png b/imgs/L8_fuerte_s1.png deleted file mode 100644 index feadf29efd125ed852344d547900332bd3aaf076..0000000000000000000000000000000000000000 Binary files a/imgs/L8_fuerte_s1.png and /dev/null differ diff --git a/imgs/L8_fuerte_s2.png b/imgs/L8_fuerte_s2.png deleted file mode 100644 index 6b4de9127d2a0813fbeffad2f06479a47a44b3d4..0000000000000000000000000000000000000000 Binary files a/imgs/L8_fuerte_s2.png and /dev/null differ diff --git a/imgs/L8_media_s1.png b/imgs/L8_media_s1.png deleted file mode 100644 index 06003101ed7edcd0f8348a8a85ab194d05e2f3ad..0000000000000000000000000000000000000000 Binary files a/imgs/L8_media_s1.png and /dev/null differ diff --git a/imgs/L8_media_s2.png b/imgs/L8_media_s2.png deleted file mode 100644 index 3db9bf788c2fc06584bd0957a098a9a3aadc8158..0000000000000000000000000000000000000000 Binary files a/imgs/L8_media_s2.png and /dev/null differ diff --git a/imgs/L8_suave_s1.png b/imgs/L8_suave_s1.png deleted file mode 100644 index 36109de45633f682ac89692bcd41086d332612e6..0000000000000000000000000000000000000000 Binary files a/imgs/L8_suave_s1.png and /dev/null differ diff --git a/imgs/L8_suave_s2.png b/imgs/L8_suave_s2.png deleted file mode 100644 index 04b4faca8fa2df2fca2411fda1b187a38ad1fdea..0000000000000000000000000000000000000000 Binary files a/imgs/L8_suave_s2.png and /dev/null differ diff --git a/imgs/L9_fuerte_s1.png b/imgs/L9_fuerte_s1.png deleted file mode 100644 index 2acca74e437c5cd19171ac04756462fe2a7620ce..0000000000000000000000000000000000000000 Binary files a/imgs/L9_fuerte_s1.png and /dev/null differ diff --git a/imgs/L9_fuerte_s2.png b/imgs/L9_fuerte_s2.png deleted file mode 100644 index e042c2a8949aebc0b2ee1b1a46870b0ec6d9c1bc..0000000000000000000000000000000000000000 Binary files a/imgs/L9_fuerte_s2.png and /dev/null differ diff --git a/imgs/L9_media_s1.png b/imgs/L9_media_s1.png deleted file mode 100644 index 310a37c4f4ff105ac72299ce3b9d49af33178b3b..0000000000000000000000000000000000000000 Binary files a/imgs/L9_media_s1.png and /dev/null differ diff --git a/imgs/L9_media_s2.png b/imgs/L9_media_s2.png deleted file mode 100644 index 97c8c773d38f74aa1b83420965f361b79dbd9686..0000000000000000000000000000000000000000 Binary files a/imgs/L9_media_s2.png and /dev/null differ diff --git a/imgs/L9_suave_s1.png b/imgs/L9_suave_s1.png deleted file mode 100644 index d60c8dcd3f3e5fd70ff63e92f9268e188bcdc16f..0000000000000000000000000000000000000000 Binary files a/imgs/L9_suave_s1.png and /dev/null differ diff --git a/imgs/L9_suave_s2.png b/imgs/L9_suave_s2.png deleted file mode 100644 index 34665fbce189e49b4fff62e32bbee2ac9a81d24e..0000000000000000000000000000000000000000 Binary files a/imgs/L9_suave_s2.png and /dev/null differ diff --git a/index.html b/index.html deleted file mode 100644 index 5cdc6c9372a2ca8dd5bbcb8f75c01c1940ff002f..0000000000000000000000000000000000000000 --- a/index.html +++ /dev/null @@ -1,188 +0,0 @@ - - - - - -v1(W_O) no es identificable - - - - -

La dirección dominante de WO no es identificable

- -

Demo de "Same function, different pruning: the dominant -direction of WO under free, soft, and hard -intervention". Una práctica habitual para podar o interpretar -cabezas de atención lee el primer vector singular de la proyección de -salida, v1(W_O), como la "dirección representativa" de -cada cabeza. Este trabajo muestra que esa dirección no está -determinada por la función: existe una libertad de gauge -W_v → W_v R, W_O → R⁻¹ W_O que deja la salida -—y el circuito W_v W_O— exactamente intactos, pero mueve -v1(W_O) a voluntad.

- -

Los ejemplos de abajo usan pesos reales de ViT-B/16 (semilla 42, la -columna ancla del paper), precalculados para 12 capas × 3 fuerzas de -gauge × 2 semillas aleatorias (72 combinaciones). Elige una -combinación y compara cuánto se mueve la dirección de peso frente a -cuánto se mueve la función.

- -
- - - -
- -matrices de redundancia antes/después del gauge - - - - - -
CantidadValor
Deriva de v1(W_O) (dirección de peso)
Deriva del circuito OV (la función)
- -
- - - - - - - diff --git a/manifiesto.sha256 b/manifiesto.sha256 new file mode 100644 index 0000000000000000000000000000000000000000..93bcb217fb4653fed48053747ec0f9082cf86450 --- /dev/null +++ b/manifiesto.sha256 @@ -0,0 +1,8 @@ +8412210331ae9ecb3fd6f01602f182207b684b2253d7116cd1cb70d14cd481a0 app.py +65ce0d4a55d05a741cdc3333736cc106b10fc2382d7048ca823bd612b62c8d59 artifacts/demo/sector_vo_pythia.safetensors +c17db69cdd7813d98b126767c8836e9b58cc51e8ff70ae302d8a89d653f05823 artifacts/demo/sector_vo_vitb.safetensors +06477df2d124e0ea50c13b82e2c377f6300238e16d07cd21b069cbbfb087eba9 demo/portador.py +9120d83454ba24d0f79f035502baa801db8fa95bca5228ae52aadc26c506d91a requirements.txt +a8466e6caad0a88ad8b1e9345fe8a2eae304ea4fd0d0e2d64fb77c6c3f9ec3ef src/firma_funcional.py +d9f6a8651fb43ad513f9aeaed94dd3563bb06c8dbcda090182669bf82ac371b4 src/gauge_flip.py +d1cf13ead126227c1423035e9af73b73860881d48dd568271d789ce38c6d95de src/nucleo_lectura.py diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..045ab52d20f8d5deaf01a7c2718163642bd02764 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +# entorno del Space: la demo es álgebra sobre pesos. no necesita +# transformers, timm ni pandas; torch en su rueda de cpu basta. +--extra-index-url https://download.pytorch.org/whl/cpu +gradio==6.22.0 +torch==2.11.0 +safetensors==0.6.2 +numpy +matplotlib +tqdm diff --git a/src/firma_funcional.py b/src/firma_funcional.py new file mode 100644 index 0000000000000000000000000000000000000000..b564633116238cc5b50eff41dcc6ebc239373dfe --- /dev/null +++ b/src/firma_funcional.py @@ -0,0 +1,415 @@ +"""núcleo compartido del portador funcional (B), gauge-invariante. + +provee la captura del contexto a_h v_h por capa, la lectura de w_o por +cabeza y las firmas de respuesta —parche y cls— como primer vector +singular derecho de la contribución al residuo c_h^p = a_h v_h w_o. +lo importan la premisa (gauge-flip, computado/estático) y la sonda del +regularizador del paper v5. +""" + +import torch +from tqdm import tqdm + + +class CapturaContexto: + """captura la entrada a proj (contexto a_h v_h) por capa. + + registra un forward-pre-hook en cada proj del encoder y guarda la + entrada sin desconectarla del grafo, de modo que el gradiente del + regularizador fluya por a_h, v_h y w_o. la entrada a proj es ya el + contexto concatenado, así que no hace falta materializar la atención. + """ + + def __init__(self, modelo, n_cabezas: int, dim_cabeza: int): + self.h = n_cabezas + self.dh = dim_cabeza + self.contexto: dict[int, torch.Tensor] = {} + self.handles = [] + for idx, bloque in enumerate(modelo.blocks): + self.handles.append( + bloque.attn.proj.register_forward_pre_hook( + self._gancho(idx))) + + def _gancho(self, idx: int): + # se reordena la entrada (b, n, c) a (b, n, h, dh) + def hook(modulo, entrada): + x = entrada[0] + b, n, _ = x.shape + self.contexto[idx] = x.view(b, n, self.h, self.dh) + return hook + + def limpiar(self) -> None: + self.contexto = {} + + def quitar(self) -> None: + for handle in self.handles: + handle.remove() + + +def w_o_por_cabeza( + modelo, + capa: int, + n_cabezas: int, + dim_cabeza: int, +) -> torch.Tensor: + """proyección de salida por cabeza w_o^(h) de una capa. + + args: + modelo: el vit. + capa: índice de la capa. + n_cabezas: cabezas h. + dim_cabeza: d_h. + + returns: + tensor [h, dh, d] con w_o^(h) por cabeza. + """ + w = modelo.blocks[capa].attn.proj.weight # [d, d] + return torch.stack([ + w[:, h * dim_cabeza:(h + 1) * dim_cabeza].t() + for h in range(n_cabezas)]) + + +def v1_desde_gram( + gram: torch.Tensor, + w_o: torch.Tensor, + iteraciones: int = 8, +) -> torch.Tensor: + """primer vector singular derecho de g @ w_o dado su gram en d_h. + + itera sobre el operador implícito (g w_o)^t (g w_o) sin materializar + la matriz d x d; el gram en el espacio de valor d_h ya viene sumado. + + args: + gram: tensor [h, dh, dh] con g^t g por cabeza. + w_o: tensor [h, dh, d] con la proyección de salida por cabeza. + iteraciones: pasos de iteración de potencia. + + returns: + tensor [h, d] con la firma unitaria por cabeza, en el residuo. + """ + h = gram.shape[0] + d = w_o.shape[-1] + v = torch.randn(h, d, device=gram.device, dtype=gram.dtype) + v = v / v.norm(dim=-1, keepdim=True).clamp_min(1e-8) + for _ in range(iteraciones): + a = torch.einsum("hed,hd->he", w_o, v) # w_o v -> [h, dh] + a = torch.einsum("hef,hf->he", gram, a) # gram a -> [h, dh] + v = torch.einsum("hed,he->hd", w_o, a) # w_o^t a -> [h, d] + v = v / v.norm(dim=-1, keepdim=True).clamp_min(1e-8) + return v + + +def firma_parche( + contexto: torch.Tensor, + w_o: torch.Tensor, + iteraciones: int = 8, +) -> torch.Tensor: + """firma de respuesta por parche, apilando el lote en el gram. + + args: + contexto: tensor [b, n, h, dh] de la captura (n incluye cls). + w_o: tensor [h, dh, d]. + iteraciones: pasos de iteración de potencia. + + returns: + tensor [h, d] con la firma unitaria por cabeza. + """ + # se descarta cls y se apila el lote en el eje de parches + g = contexto[:, 1:, :, :].permute(2, 0, 1, 3) # [h, b, p, dh] + g = g.reshape(g.shape[0], -1, g.shape[-1]) # [h, b*p, dh] + gram = torch.einsum("hnd,hne->hde", g, g) # [h, dh, dh] + return v1_desde_gram(gram, w_o, iteraciones) + + +def firma_cls( + contexto: torch.Tensor, + w_o: torch.Tensor, +) -> torch.Tensor: + """escritura media de la cabeza al token cls, en el residuo. + + args: + contexto: tensor [b, n, h, dh] de la captura. + w_o: tensor [h, dh, d]. + + returns: + tensor [h, d] con la firma unitaria por cabeza (vector llano). + """ + cls = contexto[:, 0, :, :] # [b, h, dh] + contrib = torch.einsum("bhe,hed->bhd", cls, w_o) # [b, h, d] + firmas = contrib.mean(dim=0) # [h, d] + return firmas / firmas.norm(dim=-1, keepdim=True).clamp_min(1e-8) + + +def w_v_por_cabeza( + modelo, + capa: int, + n_cabezas: int, + dim_cabeza: int, +) -> torch.Tensor: + """filas de valor w_v^(h) de una capa, desde el qkv fusionado. + + en timm el qkv concatena [q, k, v]; v ocupa el último tercio y se + reordena por cabeza. + + args: + modelo: el vit. + capa: índice de la capa. + n_cabezas: cabezas h. + dim_cabeza: d_h. + + returns: + tensor [h, dh, d] con w_v^(h) por cabeza. para la + convención traspuesta, usar `w_v_columnas`. + """ + w = modelo.blocks[capa].attn.qkv.weight # [3d, d] + base = 2 * w.shape[1] # inicio de v + return torch.stack([ + w[base + h * dim_cabeza:base + (h + 1) * dim_cabeza, :] + for h in range(n_cabezas)]) + + +def w_v_columnas( + modelo, + capa: int, + n_cabezas: int, + dim_cabeza: int, +) -> torch.Tensor: + """w_v^(h) traspuesta, en la convención de columnas [h, d, dh]. + + la misma matriz que `w_v_por_cabeza`, con los ejes al revés. las + dos existen porque el aparato las necesita en las dos formas ---la + factorización qr del circuito ov pide [h, d, dh]---, y llevan + nombres distintos justamente para que no se confundan: un cambio + silencioso entre ambas produce ángulos plausibles y falsos. + + args: + modelo: el vit, o cualquier portador con la misma interfaz. + capa: índice de la capa. + n_cabezas: cabezas h. + dim_cabeza: d_h. + + returns: + tensor [h, d, dh] con w_v^(h) por cabeza. + """ + return w_v_por_cabeza(modelo, capa, n_cabezas, dim_cabeza).transpose(1, 2) + + +def w_qk_por_cabeza( + modelo, + capa: int, + n_cabezas: int, + dim_cabeza: int, +) -> tuple[torch.Tensor, torch.Tensor]: + """filas de consulta y clave w_q^(h), w_k^(h) de una capa. + + args: + modelo: el vit. + capa: índice de la capa. + n_cabezas: cabezas h. + dim_cabeza: d_h. + + returns: + tupla (w_q [h, dh, d], w_k [h, dh, d]). + """ + w = modelo.blocks[capa].attn.qkv.weight # [3d, d] + d = w.shape[1] + w_q = torch.stack([ + w[h * dim_cabeza:(h + 1) * dim_cabeza, :] + for h in range(n_cabezas)]) + w_k = torch.stack([ + w[d + h * dim_cabeza:d + (h + 1) * dim_cabeza, :] + for h in range(n_cabezas)]) + return w_q, w_k + + +def v1_circuito( + izq: torch.Tensor, + der: torch.Tensor, + iteraciones: int = 8, +) -> torch.Tensor: + """primer vector singular del circuito compuesto izq·der. + + sirve para el circuito ov (w_v w_o) y el qk (w_q w_k^t); con los + factores en convención de código (izq=[dh,d]=W^T) la composición + materializa m=izq^t der = el circuito d x d, rango <= d_h e + invariante de gauge. se itera sobre m m^t sin materializar la matriz + d x d, vía los factores en d_h. + + args: + izq: tensor [h, dh, d], el factor izquierdo. + der: tensor [h, dh, d], el factor derecho. + iteraciones: pasos de iteración de potencia. + + returns: + tensor [h, d] con la firma unitaria por cabeza, invariante. + """ + h = izq.shape[0] + d = izq.shape[-1] + v = torch.randn(h, d, device=izq.device, dtype=izq.dtype) + v = v / v.norm(dim=-1, keepdim=True).clamp_min(1e-8) + # m = izq^t der ; m m^t = izq^t der der^t izq + for _ in range(iteraciones): + a = torch.einsum("hed,hd->he", izq, v) # izq v -> [h, dh] + b = torch.einsum("hed,he->hd", der, a) # der^t a -> [h, d] + c = torch.einsum("hed,hd->he", der, b) # der b -> [h, dh] + v = torch.einsum("hed,he->hd", izq, c) # izq^t c -> [h, d] + v = v / v.norm(dim=-1, keepdim=True).clamp_min(1e-8) + return v + + +@torch.no_grad() +def geometria_circuito_exacta( + modelo, + portador: str, + n_cabezas: int = 12, + dim_cabeza: int = 64, +) -> tuple[float, float]: + """theta_min medio y redundancia media del circuito, por svd exacta. + + lectura para juzgar la separación del portador de circuito ---no la + iteración de potencia del regularizador, que arrastra ruido de init---: + por capa materializa el circuito (ov = w_v w_o, qk = w_q w_k^t), toma + su primer vector singular exacto, y agrega entre capas el + theta_min (peor par, sin signo) y la redundancia media (|cos| medio + fuera de la diagonal). la redundancia acompaña al theta_min porque el + peor par puede despegarse sin que el conjunto se separe. + + args: + modelo: el vit (timm) que expone .blocks. + portador: 'ov' o 'qk'. + n_cabezas: cabezas h. + dim_cabeza: d_h. + + returns: + tupla (theta_min medio en grados, redundancia media). + """ + n_capas = len(modelo.blocks) + thetas, redund = [], [] + for l in range(n_capas): + if portador == "ov": + izq = w_v_por_cabeza(modelo, l, n_cabezas, dim_cabeza) + der = w_o_por_cabeza(modelo, l, n_cabezas, dim_cabeza) + else: + izq, der = w_qk_por_cabeza(modelo, l, n_cabezas, dim_cabeza) + circ = torch.einsum("hed,hef->hdf", izq, der) # [h, d, d] + r = torch.stack([ + torch.linalg.svd(circ[h], full_matrices=False).U[:, 0] + for h in range(n_cabezas)]) # [h, d] + fuera = ~torch.eye(n_cabezas, dtype=torch.bool, device=r.device) + cos = (r @ r.t()).abs().clamp(max=1.0) + thetas.append(float(torch.rad2deg( + torch.arccos(cos[fuera].max())))) + redund.append(float(cos[fuera].mean())) + return float(sum(thetas) / n_capas), float(sum(redund) / n_capas) + + +def contribuciones_por_cabeza( + contexto: torch.Tensor, + w_o: torch.Tensor, +) -> torch.Tensor: + """contribución al residuo a_h v_h w_o por cabeza. + + args: + contexto: tensor [b, n, h, dh] de CapturaContexto. + w_o: tensor [h, dh, d]. + + returns: + tensor [h, b, n, d] con la contribución de cada cabeza al residuo. + """ + return torch.einsum("bnhe,hed->hbnd", contexto, w_o) + + +def _cka_lineal(x: torch.Tensor, y: torch.Tensor) -> float: + """cka lineal entre dos representaciones [n, p] y [n, q], centradas. + + huella no circular de diversidad de output (no usa el primer vector + singular que el regularizador separa): 1 = idénticas, 0 = ortogonales. + + args: + x: tensor [n, p]. + y: tensor [n, q]. + + returns: + cka lineal en [0, 1]. + """ + x = x - x.mean(0, keepdim=True) + y = y - y.mean(0, keepdim=True) + num = (x.t() @ y).norm() ** 2 + den = (x.t() @ x).norm() * (y.t() @ y).norm() + return float(num / den.clamp_min(1e-12)) + + +@torch.no_grad() +def diversidad_output_cka( + modelo, + cargador, + n_cabezas: int = 12, + dim_cabeza: int = 64, +) -> tuple[float, list[float]]: + """redundancia de output por capa: cka lineal entre cabezas sobre el + flujo completo de contribuciones, sin promediar parches ni muestras. + + cada par (imagen, parche) es una muestra; para el par (h, h') se mide + el cka lineal entre $c_h, c_{h'}\\in R^{n\\times d}$ ---la contribución + por muestra, no su media---. es la huella que el regularizador OV no + optimiza directamente (dos cabezas pueden compartir su primer vector + singular y tener cka bajo si se encienden en muestras distintas), y + por eso el pago no circular. se acumula el gram cruzado por capa para + no materializar la matriz n x d completa. complementa s_func, que ve + solo el lado q.k. el suelo del cka lineal escala con d/n, así que se + reporta como delta contra el brazo base sobre el mismo conjunto. + + args: + modelo: vit (timm) en eval. + cargador: loader del conjunto congelado (mismas imágenes y orden + para todos los brazos, para que el suelo común se reste). + n_cabezas: cabezas h. + dim_cabeza: d_h. + + returns: + tupla (cka media entre capas, lista de cka por capa). + """ + modelo.eval() + disp = next(modelo.parameters()).device + captura = CapturaContexto(modelo, n_cabezas, dim_cabeza) + n_capas = len(modelo.blocks) + d = w_o_por_cabeza(modelo, 0, n_cabezas, dim_cabeza).shape[-1] + pares = [(i, j) for i in range(n_cabezas) + for j in range(i, n_cabezas)] + idx = {ij: k for k, ij in enumerate(pares)} + gram = torch.zeros(n_capas, len(pares), d, d, device=disp) + suma = torch.zeros(n_capas, n_cabezas, d, device=disp) + total = 0 + for imgs, _ in tqdm(cargador, desc="cka output"): + captura.limpiar() + _ = modelo(imgs.to(disp)) + muestras = 0 + for l in range(n_capas): + w_o = w_o_por_cabeza(modelo, l, n_cabezas, dim_cabeza) + c = contribuciones_por_cabeza(captura.contexto[l], w_o) + # se apilan (imagen, parche) como muestras, sin cls ni medias + c = c[:, :, 1:, :].reshape(n_cabezas, -1, d) # [h, b*p, d] + muestras = c.shape[1] + suma[l] = suma[l] + c.sum(dim=1) + for (i, j) in pares: + gram[l, idx[(i, j)]] += c[i].t() @ c[j] + total += muestras + captura.quitar() + + fuera = [(i, j) for i in range(n_cabezas) + for j in range(i + 1, n_cabezas)] + por_capa = [] + for l in range(n_capas): + # gram centrado: gc_ij = g_ij - (sum c_i)(sum c_j)^t / n + nrm = [ + (gram[l, idx[(i, i)]] + - torch.outer(suma[l, i], suma[l, i]) / total).norm() + for i in range(n_cabezas)] + vals = [] + for (i, j) in fuera: + gc = (gram[l, idx[(i, j)]] + - torch.outer(suma[l, i], suma[l, j]) / total) + vals.append(float( + gc.norm() ** 2 / (nrm[i] * nrm[j]).clamp_min(1e-12))) + por_capa.append(sum(vals) / len(vals)) + return float(sum(por_capa) / n_capas), por_capa diff --git a/src/gauge_flip.py b/src/gauge_flip.py new file mode 100644 index 0000000000000000000000000000000000000000..af39c13ce03d1f2d05462e021fa5cb8fdd10147d --- /dev/null +++ b/src/gauge_flip.py @@ -0,0 +1,184 @@ +"""fase g — gauge-flip valor-salida demostrado, sin reentrenar. + +aplica una transformación de gauge (en convención del paper v5: +w_v <- w_v r, w_o <- r^{-1} w_o, r en gl(d_h)) que deja la salida intacta +a precisión de máquina, y muestra que el ranking de redundancia por +v1(w_o) ---primer vector singular derecho de w_o, en r^768--- cambia +mientras la firma del circuito ov (w_v w_o) queda estable. convierte la +crítica de la premisa en una vulnerabilidad exhibida: leer la dirección +de w_o es leer ruido de gauge. +""" + +import torch + +from src.firma_funcional import w_o_por_cabeza, w_v_por_cabeza + + +@torch.no_grad() +def aplica_gauge_ov( + modelo, + capa: int, + n_cabezas: int, + dim_cabeza: int, + semilla: int = 0, + escala_id: float | None = None, +) -> float: + """aplica un gauge valor-salida por cabeza, in situ. + + sustituye w_v <- r^t w_v, b_v <- r^t b_v y w_o <- w_o r^{-t} con r en + gl(d_h) bien condicionada; deja la salida intacta a precisión de + máquina y denota la libertad de gauge del sector valor-salida. el + sesgo de valor entra en la transformación porque v = x w_v^t + b_v y + la compensación de w_o exige que b_v gire igual que la weight; en una + columna con qkv_bias omitirlo rompe la invariancia. clonar el modelo + antes si se quiere conservar el original. + + args: + modelo: el vit, modificado in situ. + capa: índice de la capa intervenida. + n_cabezas: cabezas h. + dim_cabeza: d_h. + semilla: semilla de la r aleatoria, para reproducir. + escala_id: coeficiente de la identidad en r = randn + escala_id*i; + menor valor aleja r de un múltiplo escalar de la identidad + ---gauge más fuerte, v1(w_o) deriva más, el circuito ov no---. + por defecto sqrt(d_h), el calibre de referencia. + + returns: + desviación media de r respecto a su mejor múltiplo escalar de la + identidad, sobre las cabezas; proxy de la fuerza del gauge. + """ + if escala_id is None: + escala_id = dim_cabeza ** 0.5 + g = torch.Generator(device="cpu").manual_seed(semilla) + attn = modelo.blocks[capa].attn + w_qkv = attn.qkv.weight # [3d, d] + w_proj = attn.proj.weight # [d, d] + base = 2 * w_qkv.shape[1] # inicio de v + # r e inversa en float64: con r fuerte la inv en fp32 amplifica el + # error por el número de condición y rompe la invariancia; en fp64 el + # gauge es preciso sea cual sea la fuerza, y solo se pierde el último + # casteo al dtype del peso. + ident = torch.eye(dim_cabeza, device=w_qkv.device, dtype=torch.float64) + desvs = [] + for h in range(n_cabezas): + rt = torch.randn(dim_cabeza, dim_cabeza, generator=g, + dtype=torch.float64).to(w_qkv.device) + r = rt + escala_id * ident + # desviación de r respecto a su mejor múltiplo escalar de la id + s = r.diagonal().mean() + desvs.append(float((r - s * ident).norm() + / (s.abs() * dim_cabeza ** 0.5 + 1e-8))) + r_inv_t = torch.linalg.inv(r).t() + fil = slice(base + h * dim_cabeza, base + (h + 1) * dim_cabeza) + col = slice(h * dim_cabeza, (h + 1) * dim_cabeza) + w_qkv[fil, :] = ( + r.t() @ w_qkv[fil, :].double()).to(w_qkv.dtype) # w_v + if attn.qkv.bias is not None: + attn.qkv.bias[fil] = ( + r.t() @ attn.qkv.bias[fil].double()).to(w_qkv.dtype) # b_v + w_proj[:, col] = ( + w_proj[:, col].double() @ r_inv_t).to(w_proj.dtype) # w_o + return float(sum(desvs) / len(desvs)) + + +@torch.no_grad() +def cos_pares_v1_wo( + modelo, + capa: int, + n_cabezas: int, + dim_cabeza: int, +) -> torch.Tensor: + """|cos| entre las direcciones v1(w_o) de las cabezas de una capa. + + v1(w_o) es el primer vector singular derecho de w_o (en r^768), la + cantidad que la poda o interpretación por dirección de salida lee; el + experimento muestra que cambia bajo el gauge. + + args: + modelo: el vit. + capa: índice de la capa. + n_cabezas: cabezas h. + dim_cabeza: d_h. + + returns: + tensor [h, h] con |cos| entre direcciones dominantes de w_o. + """ + w_o = w_o_por_cabeza(modelo, capa, n_cabezas, dim_cabeza) # [h,dh,d] + u = torch.stack([ + torch.linalg.svd(w_o[h], full_matrices=False).Vh[0] + for h in range(n_cabezas)]) # [h, d] + return (u @ u.t()).abs() + + +@torch.no_grad() +def cos_pares_circuito_ov( + modelo, + capa: int, + n_cabezas: int, + dim_cabeza: int, +) -> torch.Tensor: + """|cos| entre las firmas invariantes del circuito ov por cabeza. + + contrasta con cos_pares_v1_wo: no cambia bajo el gauge. el + diagnóstico usa svd exacta del circuito ov (w_v w_o) ---no la + iteración de potencia del regularizador---, de modo que la + invariancia se ve a precisión de máquina y no arrastra el ruido de + init de la potencia. + + args: + modelo: el vit. + capa: índice de la capa. + n_cabezas: cabezas h. + dim_cabeza: d_h. + + returns: + tensor [h, h] con |cos| entre firmas del circuito ov. + """ + w_v = w_v_por_cabeza(modelo, capa, n_cabezas, dim_cabeza) # [h,dh,d] + w_o = w_o_por_cabeza(modelo, capa, n_cabezas, dim_cabeza) # [h,dh,d] + # circuito ov por cabeza m = w_v w_o (rango <= dh, invariante de + # gauge); su primer vector singular es la firma invariante. el einsum + # contrae el eje de valor d_h compartido (w_v es [dh,d]=W_v^T) + circ = torch.einsum("hed,hef->hdf", w_v, w_o) # [h, d, d] + r = torch.stack([ + torch.linalg.svd(circ[h], full_matrices=False).U[:, 0] + for h in range(n_cabezas)]) # [h, d] + return (r @ r.t()).abs() + + +@torch.no_grad() +def aplica_gauge_ortogonal( + modelo, capa: int, n_cabezas: int, dim_cabeza: int, semilla: int +) -> None: + """aplica un gauge ortogonal por cabeza, in situ. + + espeja `aplica_gauge_ov` salvo que r se muestrea en o(d_h) en vez + de en gl(d_h). sirve de assert de c1: bajo este gauge no debe + moverse ni v1(w_o) ni el circuito. + + Args: + modelo: el vit. + capa: índice de la capa. + n_cabezas: cabezas h. + dim_cabeza: d_h. + semilla: semilla del generador de r. + """ + g = torch.Generator(device="cpu").manual_seed(semilla) + attn = modelo.blocks[capa].attn + w_qkv, w_proj = attn.qkv.weight, attn.proj.weight + base = 2 * w_qkv.shape[1] + for h in range(n_cabezas): + m = torch.randn(dim_cabeza, dim_cabeza, generator=g, + dtype=torch.float64) + r = torch.linalg.qr(m)[0] # ortogonal exacta + r_inv_t = r # (r^-1)^t = r + fil = slice(base + h * dim_cabeza, + base + (h + 1) * dim_cabeza) + col = slice(h * dim_cabeza, (h + 1) * dim_cabeza) + w_qkv[fil, :] = (r.t() @ w_qkv[fil, :].double()).to(w_qkv.dtype) + if attn.qkv.bias is not None: + attn.qkv.bias[fil] = ( + r.t() @ attn.qkv.bias[fil].double()).to(w_qkv.dtype) + w_proj[:, col] = ( + w_proj[:, col].double() @ r_inv_t).to(w_proj.dtype) diff --git a/src/nucleo_lectura.py b/src/nucleo_lectura.py new file mode 100644 index 0000000000000000000000000000000000000000..914c22b085f8f210dff3f15a51f36582a05d1deb --- /dev/null +++ b/src/nucleo_lectura.py @@ -0,0 +1,61 @@ +"""núcleo de lectura: svd exacta y decisión de poda, sin dependencias. + +`firma_exacta` y `decisiones` las comparten el aparato del paper y la +demo. vivían en `scripts/run_fase_G.py` y `scripts/decision_rota.py`, +que arrastran timm y pandas por su cadena de imports; aquí solo hace +falta torch, de modo que el Space puede ejecutar **el mismo código** +sin cargar el entorno de investigación entero. los cuerpos son los que +produjeron las tablas 2 y 3, movidos sin tocar una línea. +""" + +import torch + +K_PODA = 3 # top-k de poda del ancla vit-b (25 % de 12 cabezas) + + +@torch.no_grad() +def firma_exacta(matriz: torch.Tensor, lado: str) -> torch.Tensor: + """vector singular dominante por svd exacta (no iteración). + + el diagnóstico de invariancia exige svd exacta: medirla con + iteración de potencia introduce ruido de init que la enmascara. + + Args: + matriz: tensor [h, m, n] con h matrices. + lado: 'izq' devuelve u[:,0] (en R^m); 'der' devuelve vh[0] + (en R^n). + + Returns: + tensor [h, k] con la dirección unitaria por cabeza. + """ + out = [] + for h in range(matriz.shape[0]): + u, _, vh = torch.linalg.svd(matriz[h], full_matrices=False) + out.append(u[:, 0] if lado == "izq" else vh[0]) + return torch.stack(out) + + +def decisiones(firmas: torch.Tensor, + k: int) -> tuple[tuple[int, int], list[int]]: + """par más redundante y top-k de poda desde unas firmas. + + Args: + firmas: tensor [h, d] de direcciones unitarias por cabeza. + k: tamaño del conjunto de poda. **sin valor por + defecto a propósito**: un default de módulo hizo + que la columna de lenguaje calculara conjuntos de + 3 y dividiera por 4 durante dos versiones del + paper. cada llamante declara el suyo. + + Returns: + tupla (par_top ordenado, top-k de cabezas por redundancia + media descendente). + """ + c = (firmas @ firmas.t()).abs().clamp(max=1.0) + c.fill_diagonal_(0.0) + h = c.shape[0] + idx = int(torch.argmax(c).item()) + par = tuple(sorted((idx // h, idx % h))) + red_media = c.sum(dim=1) / (h - 1) + topk = torch.argsort(red_media, descending=True)[:k] + return par, [int(i) for i in topk] diff --git a/style.css b/style.css deleted file mode 100644 index 114adf441e9032febb46bc056b2a8bb651075f0d..0000000000000000000000000000000000000000 --- a/style.css +++ /dev/null @@ -1,28 +0,0 @@ -body { - padding: 2rem; - font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif; -} - -h1 { - font-size: 16px; - margin-top: 0; -} - -p { - color: rgb(107, 114, 128); - font-size: 15px; - margin-bottom: 10px; - margin-top: 5px; -} - -.card { - max-width: 620px; - margin: 0 auto; - padding: 16px; - border: 1px solid lightgray; - border-radius: 16px; -} - -.card p:last-child { - margin-bottom: 0; -}