File size: 10,953 Bytes
5c6c847 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | """ZeroPoint:分层记忆槽网络。
设计文档见 docs/ZeroPoint.md。核心是 MemoryMixer(代替 Transformer 里的自注意力):
每层有 H 个头,每个头有 M 个记忆槽 S[m](D 维向量),每个槽有自己的保留率 λ[m]。
读到第 t 个字时:
w_t = softmax(x_t Ww) * sigmoid(x_t Wg) 写地址(槽之间竞争)× 写强度
S[m] ← λ[m] · S[m] + w_t[m] · v_t 所有槽先按自己的速度遗忘,再写入
a_t = softmax(x_t Wr) 读地址
r_t = Σ_m a_t[m] · S[m] 从槽里读出
两种等价算法:
forward() 分块并行,训练用。序列切成每块 CHUNK 个字:
块内 r = (A ⊙ 因果掩码) @ v,A[i,j] = Σ_m a_i[m] w_j[m] λ[m]^(i-j)
利用 λ^(i-j) = λ^i · λ^(-j),一次矩阵乘
跨块 r += (a ⊙ λ^(i+1)) @ S_in S_in = 进入这一块时的槽状态
块尾 S_out = λ^c ⊙ S_in + (w ⊙ λ^(c-1-j))ᵀ @ v
λ^(-j) 最大 2^(CHUNK-1),不会溢出,所以序列多长都行
step() 递推形式,推理用:每个字 O(H·M·D),状态大小固定,没有 KV 缓存
槽的初始状态 S0 是可学习参数(不是全 0),这样序列开头读出来的向量不会接近 0、被 LayerNorm 放大噪声。
ema=True(指数滑动平均写入):S[m] ← λ[m]·S[m] + (1-λ[m])·w_t[m]·v_t
槽的值永远落在"写进去过的值"的范围里,不会随序列变长而越积越大。
系统直觉模型要连续跑几天,用这个;实现上只是把写地址 w 乘上 (1-λ),并行和递推两种形式都不用改。
"""
import numpy as np
from zerocg import Tensor, concat
from zerocg import functional as F
from zerocg import nn
from .transformer import sinusoidal_encoding
LAMBDA_MIN = 0.5 # λ = 0.5 + 0.5·sigmoid(θ) ∈ (0.5, 1)
CHUNK = 32 # 分块并行的块长:块内 λ^(-j) 最大 2^31,float32 绰绰有余
def _layernorm_np(x, ln):
mu = x.mean(-1, keepdims=True)
var = ((x - mu) ** 2).mean(-1, keepdims=True)
return (x - mu) / np.sqrt(var + ln.eps) * ln.weight.data + ln.bias.data
def _softmax_np(x):
e = np.exp(x - x.max(-1, keepdims=True))
return e / e.sum(-1, keepdims=True)
def _gelu_np(a):
return 0.5 * a * (1 + np.tanh(np.sqrt(2 / np.pi) * (a + 0.044715 * a ** 3)))
class MemoryMixer(nn.Module):
"""参数里的开关只给消融实验用(experiments/ablation.py),默认值就是 ZeroPoint 本身:
gate 写强度门
write "softmax":写入时槽之间竞争;"sigmoid":每个槽独立决定写不写
init "hier":半衰期按存储层级铺开;"uniform":所有槽一样
learn_lambda 保留率 λ 是否可学
ema 写入按 (1-λ) 缩放(指数滑动平均),长时间运行不漂移
"""
def __init__(self, n_embd, n_head, n_slot, max_half_life=64.0,
gate=True, write="softmax", init="hier", learn_lambda=True, ema=False):
assert n_embd % n_head == 0 and write in ("softmax", "sigmoid") and init in ("hier", "uniform")
self.H, self.M, self.D = n_head, n_slot, n_embd // n_head
self.write_mode = write
self.ema = ema
self.read = nn.Linear(n_embd, n_head * n_slot)
self.write = nn.Linear(n_embd, n_head * n_slot)
self.gate = nn.Linear(n_embd, n_head) if gate else None
self.value = nn.Linear(n_embd, n_embd)
self.out = nn.Linear(n_embd, n_embd)
self.norm = nn.LayerNorm(self.D) # 每个头读出来的向量各自归一化(不同槽的量级差别很大)
# 初始化成"存储层级":槽 0 的半衰期约 1.5 个字(像寄存器),最后一个约 max_half_life 个字(像内存)
if init == "hier":
half_life = np.geomspace(1.5, max_half_life, n_slot)
else: # 对照:所有槽都取层级的几何中点
half_life = np.full(n_slot, np.sqrt(1.5 * max_half_life))
lam = 0.5 ** (1 / half_life)
theta = np.tile(np.log((2 * lam - 1) / (2 - 2 * lam)), (n_head, 1)) # sigmoid 的反函数,(H, M)
self.theta = nn.Parameter(theta) if learn_lambda else Tensor(theta)
self.s0 = nn.Parameter(np.random.randn(n_head, n_slot, self.D) * 0.1) # 可学习的初始状态
self.usage = None # 最近一次 forward 的 (读地址, 写地址) 平均值,各 (H, M),用来查槽有没有塌缩
def retention(self):
"""每个槽的保留率 λ,形状 (H, M)。"""
return F.sigmoid(self.theta) * (1 - LAMBDA_MIN) + LAMBDA_MIN
def forward(self, x):
B, T, C = x.shape
H, M, D = self.H, self.M, self.D
a = F.softmax(self.read(x).reshape(B, T, H, M), axis=-1).transpose(0, 2, 1, 3) # (B,H,T,M)
w = self.write(x).reshape(B, T, H, M)
w = F.softmax(w, axis=-1) if self.write_mode == "softmax" else F.sigmoid(w)
if self.gate is not None:
w = w * F.sigmoid(self.gate(x)).reshape(B, T, H, 1)
w = w.transpose(0, 2, 1, 3) # (B,H,T,M)
v = self.value(x).reshape(B, T, H, D).transpose(0, 2, 1, 3) # (B,H,T,D)
self.usage = (a.data.mean(axis=(0, 2)), w.data.mean(axis=(0, 2)))
lam = self.retention() # (H, M)
if self.ema:
w = w * (1 - lam).reshape(1, H, 1, M)
log_lam = lam.log().reshape(H, 1, M)
S = self.s0.reshape(1, H, M, D) # 进入第一块时的状态(对 batch 广播)
outs = []
for s in range(0, T, CHUNK):
e = min(s + CHUNK, T)
c = e - s
ac, wc, vc = a[:, :, s:e], w[:, :, s:e], v[:, :, s:e]
i = Tensor(np.arange(c, dtype=np.float64).reshape(1, c, 1))
decay = (log_lam * i).exp() # λ^i, (H,c,M)
A = (ac * decay) @ (wc * (log_lam * -i).exp()).transpose(0, 1, 3, 2) # (B,H,c,c)
A = A.masked_fill(np.triu(np.ones((c, c), dtype=bool), 1), 0.0)
outs.append(A @ vc + (ac * decay * lam.reshape(H, 1, M)) @ S) # 块内 + 跨块
if e < T:
tail = (log_lam * Tensor(np.arange(c - 1, -1, -1, dtype=np.float64).reshape(1, c, 1))).exp()
S = (lam ** c).reshape(1, H, M, 1) * S + (wc * tail).transpose(0, 1, 3, 2) @ vc
r = outs[0] if len(outs) == 1 else concat(outs, axis=2)
return self.out(self.norm(r).transpose(0, 2, 1, 3).reshape(B, T, C))
# ---------- 递推形式(numpy,无梯度)----------
def init_state(self, B):
return np.broadcast_to(self.s0.data, (B, self.H, self.M, self.D)).copy()
def step(self, x, S):
"""x: (B, C) 一个字的输入;S: (B, H, M, D) 记忆槽。返回 (输出, 新状态)。"""
B = x.shape[0]
H, M, D = self.H, self.M, self.D
lin = lambda layer, z: z @ layer.weight.data + layer.bias.data # noqa: E731
a = _softmax_np(lin(self.read, x).reshape(B, H, M))
w = lin(self.write, x).reshape(B, H, M)
w = _softmax_np(w) if self.write_mode == "softmax" else F.sigmoid_np(w)
if self.gate is not None:
w = w * F.sigmoid_np(lin(self.gate, x))[:, :, None]
v = lin(self.value, x).reshape(B, H, D)
lam = self.retention().data
if self.ema:
w = w * (1 - lam)[None]
self.last_addr = (a, w) # 诊断用:这一步的读地址和写地址,(B, H, M)
S = lam[None, :, :, None] * S + w[..., None] * v[:, :, None, :]
r = np.einsum("bhm,bhmd->bhd", a, S)
r = _layernorm_np(r, self.norm).reshape(B, H * D)
return lin(self.out, r), S
class Block(nn.Module):
def __init__(self, n_embd, n_head, n_slot, dropout=0.0, mlp_hidden=None, **mixer_opts):
hidden = mlp_hidden or 4 * n_embd
self.ln1 = nn.LayerNorm(n_embd)
self.mix = MemoryMixer(n_embd, n_head, n_slot, **mixer_opts)
self.ln2 = nn.LayerNorm(n_embd)
self.fc1 = nn.Linear(n_embd, hidden)
self.fc2 = nn.Linear(hidden, n_embd)
self.drop = nn.Dropout(dropout)
def forward(self, x):
x = x + self.drop(self.mix(self.ln1(x)))
return x + self.drop(self.fc2(F.gelu(self.fc1(self.ln2(x)))))
def step(self, x, S):
h, S = self.mix.step(_layernorm_np(x, self.ln1), S)
x = x + h
h = _gelu_np(_layernorm_np(x, self.ln2) @ self.fc1.weight.data + self.fc1.bias.data)
return x + h @ self.fc2.weight.data + self.fc2.bias.data, S
class ZeroX(nn.Module):
"""字符级语言模型。默认没有位置编码:位置信息来自各个槽不同的遗忘速度。
pos="sin" 只给消融实验用:额外加上论文的正弦位置编码,看看还有没有用。
mlp_hidden 默认 4 × n_embd;消融实验用它把各变体的参数量对齐。"""
def __init__(self, vocab_size, n_embd=64, n_head=4, n_slot=16, n_layer=2, dropout=0.0,
pos="none", mlp_hidden=None, **mixer_opts):
assert pos in ("none", "sin")
self.pos_kind = pos
self.tok = nn.Embedding(vocab_size, n_embd)
self.blocks = [Block(n_embd, n_head, n_slot, dropout, mlp_hidden, **mixer_opts) for _ in range(n_layer)]
self.ln_f = nn.LayerNorm(n_embd)
self.head = nn.Linear(n_embd, vocab_size)
def forward(self, idx):
x = self.tok(idx)
if self.pos_kind == "sin":
x = x + Tensor(sinusoidal_encoding(idx.shape[1], x.shape[-1]))
for b in self.blocks:
x = b(x)
return self.head(self.ln_f(x))
def init_state(self, B):
return [b.mix.init_state(B) for b in self.blocks]
def step(self, idx_t, states, t=0):
"""idx_t: (B,) 第 t 个字。返回 (logits (B, V), 新状态)。"""
x = self.tok.weight.data[idx_t]
if self.pos_kind == "sin":
x = x + sinusoidal_encoding(t + 1, x.shape[-1])[t]
new = []
for b, S in zip(self.blocks, states):
x, S = b.step(x, S)
new.append(S)
return _layernorm_np(x, self.ln_f) @ self.head.weight.data + self.head.bias.data, new
def forward_recurrent(self, idx):
"""逐字递推跑完整个序列,返回 (B, T, V) 的 logits。"""
idx = np.asarray(idx)
states = self.init_state(idx.shape[0])
out = []
for t in range(idx.shape[1]):
logits, states = self.step(idx[:, t], states, t)
out.append(logits)
return np.stack(out, axis=1)
def state_floats(self):
"""推理时需要保存的状态大小(浮点数个数),和序列长度无关。"""
return sum(b.mix.H * b.mix.M * b.mix.D for b in self.blocks)
|