PowerMachine commited on
Commit
7ff1796
·
verified ·
1 Parent(s): 48a9e16

HAKO upload: hako/core/heads.py

Browse files
Files changed (1) hide show
  1. hako/core/heads.py +156 -0
hako/core/heads.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CPNN / BKN / DASOM heads coupled to GHSOM neurons (L3 plugins).
2
+
3
+ Theorem T-HEADS (convergence with stationary inputs -- frozen sources).
4
+ The frozen ONNX sources give z with d z / d W_src = 0 (LLM01 Phase-1
5
+ exhaustion lemma). The per-neuron update combines Kohonen attraction and
6
+ task gradients,
7
+ Delta W_j = eta_G h_cj (z - W_j)
8
+ + eta_D h_cj dL_Denoise/dW_j + eta_B h_cj dL_Bidir/dW_j,
9
+ and analogously for V (CPNN supervised regression) and U (BKN/DASOM).
10
+ With step sizes eta_t -> 0, sum eta_t = inf, sum eta_t^2 < inf (Robbins-Monro
11
+ schedule implemented by the trainer), the recursion converges a.s. to the
12
+ stationary set; for the Kohonen component the stationary points are the
13
+ Voronoi centroids of the input distribution restricted to each winner cell
14
+ (classical SOM result: at stationarity E[ h_cj (z - W_j) ] = 0 forces W_j to
15
+ the local conditional mean). The head layers V, U converge under plain SGD on
16
+ their convex quadratic losses (least squares with fixed features), whose
17
+ objective is strongly convex in (V, U) given bounded activations.
18
+ QED.
19
+
20
+ Outputs per forward (arch.pdf 2.1):
21
+ CPNN : y_hat = sum_j h_cj V_j z (smoothed regression)
22
+ BKN : z_rec = sum_j h_cj U^B_j z (bidirectional reconstruction)
23
+ DASOM: z_den = sum_j h_cj U^D_j x_noisy (denoising reconstruction)
24
+ """
25
+ from __future__ import annotations
26
+
27
+ from typing import Dict, Iterator, Tuple
28
+
29
+ import torch
30
+ import torch.nn as nn
31
+
32
+
33
+ def neuron_key(nid: int, idx: int) -> str:
34
+ return f"{nid}:{idx}"
35
+
36
+
37
+ class HeadsModule(nn.Module):
38
+ """Lazy per-neuron head tensors shared by all ensemble replicas.
39
+
40
+ Memory bound (aggressive management): V is out_dim x N, U^B and U^D are
41
+ diagonal + rank-r factorizations r=16, so per neuron we store
42
+ O(out*N + 2*(N*r + N)) floats, NOT 2*N^2. For N=256, r=16 this is ~50 KiB
43
+ per neuron, allowing hundreds of neurons within the RAM cap.
44
+ """
45
+
46
+ def __init__(self, N_dim: int, out_dim: int = 64, rank: int = 16,
47
+ n_replicas: int = 2, seed: int = 7) -> None:
48
+ super().__init__()
49
+ self.N = N_dim
50
+ self.out_dim = out_dim
51
+ self.rank = rank
52
+ self.K = n_replicas
53
+ g = torch.Generator().manual_seed(seed)
54
+ self._gen = g
55
+ self.V: Dict[str, torch.nn.Parameter] = {}
56
+ self.Udiag: Dict[Tuple[str, int], torch.nn.Parameter] = {}
57
+ self.Ua: Dict[Tuple[str, int], torch.nn.Parameter] = {}
58
+ self.Ub: Dict[Tuple[str, int], torch.nn.Parameter] = {}
59
+
60
+ # ------------------------------------------------------------- creation
61
+ def attach(self, key: str) -> None:
62
+ if key in self.V:
63
+ return
64
+ self.V[key] = nn.Parameter(0.02 * torch.randn(self.out_dim, self.N,
65
+ generator=self._gen))
66
+ for rep in range(self.K):
67
+ self.Udiag[(key, rep)] = nn.Parameter(torch.ones(self.N))
68
+ self.Ua[(key, rep)] = nn.Parameter(
69
+ 0.02 * torch.randn(self.N, self.rank, generator=self._gen))
70
+ self.Ub[(key, rep)] = nn.Parameter(
71
+ 0.02 * torch.randn(self.rank, self.N, generator=self._gen))
72
+
73
+ def attach_parent_mean(self, key: str, W_parent_mean: torch.Tensor) -> None:
74
+ """New neurons after growth: V initialized from parent-cell projection."""
75
+ self.attach(key)
76
+ with torch.no_grad():
77
+ self.V[key].zero_()
78
+ # project parent mean onto the dominant output coordinates
79
+ self.V[key][:, : min(self.N, self.out_dim)] = \
80
+ W_parent_mean[: min(self.N, self.out_dim), : min(self.N, self.out_dim)].t()
81
+
82
+ def parameters(self, recurse: bool = True) -> Iterator[torch.nn.Parameter]:
83
+ for p in self.V.values():
84
+ yield p
85
+ for d in (self.Udiag, self.Ua, self.Ub):
86
+ for p in d.values():
87
+ yield p
88
+
89
+ # -------------------------------------------------------------- forward
90
+ def _U(self, key: str, rep: int) -> torch.Tensor:
91
+ d = self.Udiag[(key, rep)]
92
+ a = self.Ua[(key, rep)]
93
+ b = self.Ub[(key, rep)]
94
+ return torch.diag(d) + a @ b
95
+
96
+ def forward(self, z: torch.Tensor, path, h_map: Dict[Tuple[int, int], torch.Tensor],
97
+ rep: int = 0) -> Dict[str, torch.Tensor]:
98
+ """z: (N,), path: [(nid, idx)...], h_map: per-node neighborhood matrix.
99
+
100
+ h_map[nid] is (rows, cols) SOM neighborhood of the winner at that node.
101
+ """
102
+ H_cat: Dict[str, torch.Tensor] = {}
103
+ y_hat = torch.zeros(self.out_dim, dtype=z.dtype)
104
+ z_rec = torch.zeros(self.N, dtype=z.dtype)
105
+ z_den = torch.zeros(self.N, dtype=z.dtype)
106
+ wsum = torch.zeros(())
107
+ for (nid, idx) in path:
108
+ key = neuron_key(nid, idx)
109
+ self.attach(key)
110
+ node_h = h_map.get(nid)
111
+ if node_h is None:
112
+ w = torch.ones(())
113
+ else:
114
+ w = node_h.flatten()[idx]
115
+ wsum = wsum + w
116
+ y_hat = y_hat + w * (self.V[key] @ z)
117
+ z_rec = z_rec + w * (self._U(key, rep) @ z)
118
+ z_den = z_den + w * (self._U(key, min(rep + 1, self.K - 1)) @ z)
119
+ H_cat[key] = z
120
+ if float(wsum) > 0:
121
+ y_hat, z_rec, z_den = y_hat / wsum, z_rec / wsum, z_den / wsum
122
+ return {"y_hat": y_hat, "z_rec": z_rec, "z_den": z_den,
123
+ "H_cat": H_cat, "wsum": wsum}
124
+
125
+ # ---------------------------------------------------------- checkpoint
126
+ def state_numpy(self) -> Dict[str, "object"]:
127
+ import numpy as np
128
+ out = {}
129
+ for k, p in self.V.items():
130
+ out[f"V[{k}]"] = p.detach().numpy()
131
+ for (k, rep), p in self.Udiag.items():
132
+ out[f"Udiag[{k}|{rep}]"] = p.detach().numpy()
133
+ for (k, rep), p in self.Ua.items():
134
+ out[f"Ua[{k}|{rep}]"] = p.detach().numpy()
135
+ for (k, rep), p in self.Ub.items():
136
+ out[f"Ub[{k}|{rep}]"] = p.detach().numpy()
137
+ return out
138
+
139
+ def load_numpy(self, state) -> None:
140
+ for name, arr in state.items():
141
+ if name.startswith("V["):
142
+ key = name[2:-1]
143
+ self.attach(key)
144
+ self.V[key].data = torch.as_tensor(arr)
145
+ elif name.startswith("Udiag["):
146
+ key, rep = name[7:-1].split("|")
147
+ self.attach(key)
148
+ self.Udiag[(key, int(rep))].data = torch.as_tensor(arr)
149
+ elif name.startswith("Ua["):
150
+ key, rep = name[4:-1].split("|")
151
+ self.attach(key)
152
+ self.Ua[(key, int(rep))].data = torch.as_tensor(arr)
153
+ elif name.startswith("Ub["):
154
+ key, rep = name[4:-1].split("|")
155
+ self.attach(key)
156
+ self.Ub[(key, int(rep))].data = torch.as_tensor(arr)