"""【未验证 · Transformer 档】真实 MNIST 训练玩具 ViT + S 结构分区函数化。
一句话结论:
Transformer 的注意力头是「并行功能模块」,不能像小 MLP 那样按权重几何
混聚,而应按结构边界(头/层)直接切分。本例在 MNIST 上训练一个玩具 ViT,
抽出注意力 Q 投影权重做 S 结构分区(按头切、头间不混聚),走完整
「训练 → 模型 → 函数化 → 生成物 → 部署使用」链路,并对照「G 几何分区会
跨头混聚」的错误示范。真实大 Transformer(ViT/LLM)精度未验证。
对应分型表第三行:Transformer = 结构分区 S(接口已实现,待真实 Transformer 验证)。
"""
from _common import (accuracy, device, load_mnist, make_drop_guard,
show_artifacts, show_value, train_model)
import math
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import nrfunc
class TransformerBlock(nn.Module):
"""玩具 ViT 的一个 Transformer 块(pre-norm,手写注意力)。
手写 Q/K/V/O 四个 Linear(而非 nn.MultiheadAttention),是为了让 Q 投影
权重 `wq.weight` 是标准 (d_model, d_model),从而能干净地按「头边界」切分:
每 head_dim 个输出神经元属一个头。
"""
def __init__(self, d_model, n_heads, ffn):
super().__init__()
self.n_heads = n_heads
self.head_dim = d_model // n_heads
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.wq = nn.Linear(d_model, d_model)
self.wk = nn.Linear(d_model, d_model)
self.wv = nn.Linear(d_model, d_model)
self.proj = nn.Linear(d_model, d_model)
self.fc1 = nn.Linear(d_model, ffn)
self.fc2 = nn.Linear(ffn, d_model)
def _attn(self, x):
B, T, D = x.shape
h, hd = self.n_heads, self.head_dim
q = self.wq(x).reshape(B, T, h, hd).transpose(1, 2)
k = self.wk(x).reshape(B, T, h, hd).transpose(1, 2)
v = self.wv(x).reshape(B, T, h, hd).transpose(1, 2)
attn = (q @ k.transpose(-2, -1)) / math.sqrt(hd)
attn = attn.softmax(-1)
out = (attn @ v).transpose(1, 2).reshape(B, T, D)
return self.proj(out)
def forward(self, x):
x = x + self._attn(self.norm1(x))
x = x + self.fc2(F.relu(self.fc1(self.norm2(x))))
return x
class ToyViT(nn.Module):
"""玩具 ViT:把 28×28 MNIST 切 7×7 patch,class token + 2 层 Transformer。"""
def __init__(self, patch=7, d_model=64, n_heads=4, n_layers=2, ffn=128,
n_cls=10):
super().__init__()
self.patch = patch
self.d_model = d_model
self.n_heads = n_heads
self.head_dim = d_model // n_heads
n_patches = (28 // patch) ** 2
self.patch_embed = nn.Linear(patch * patch, d_model)
self.cls_token = nn.Parameter(torch.zeros(1, 1, d_model))
self.pos_embed = nn.Parameter(torch.zeros(1, n_patches + 1, d_model))
self.blocks = nn.ModuleList(
[TransformerBlock(d_model, n_heads, ffn) for _ in range(n_layers)])
self.norm = nn.LayerNorm(d_model)
self.head = nn.Linear(d_model, n_cls)
def forward(self, x):
B = x.shape[0]
x = F.unfold(x, kernel_size=self.patch, stride=self.patch)
x = x.transpose(1, 2)
x = self.patch_embed(x)
cls = self.cls_token.expand(B, -1, -1)
x = torch.cat([cls, x], dim=1)
x = x + self.pos_embed
for blk in self.blocks:
x = blk(x)
x = self.norm(x)
return self.head(x[:, 0])
def main():
dev = device()
print(f'设备:{dev}')
torch.manual_seed(0)
torch.cuda.manual_seed(0)
train_x, train_y, test_x, test_y = load_mnist()
train_x = train_x[:20000].reshape(-1, 1, 28, 28)
test_x4 = test_x.reshape(-1, 1, 28, 28)
train_y = train_y[:20000]
print('\n① 训练玩具 ViT(patch=7, d_model=64, 4 头, 2 层, 20000 样本):')
model = train_model(ToyViT(), train_x, train_y, epochs=10, seed=0, dev=dev)
acc_before = accuracy(model, test_x4, test_y, dev)
print(f'② 函数化前 · 测试准确率 = {acc_before * 100:.2f}%')
W = model.blocks[0].wq.weight.detach().cpu().numpy().astype(np.float64)
n_heads, head_dim = model.n_heads, model.head_dim
groups = np.repeat(np.arange(n_heads), head_dim)
guard = make_drop_guard(model, model.blocks[0].wq, W.shape, test_x4, test_y,
dev, acc_before, tol_pt=3.0)
alloc_s = nrfunc.auto_alloc(W, signal='S', groups=groups, eval_fn=guard, seed=0)
print(f'\n③ [S 结构分区 · auto_alloc 自动选] decision={alloc_s["decision"]} '
f'order={alloc_s["order"]} K={alloc_s["K"]}(= 头数)r={alloc_s["r"]},'
f'内存省 {alloc_s["saving"]*100:.1f}%')
if alloc_s['decision'] == 'skip':
print(f' → 该层被判跳过:{alloc_s["skipped_reason"]}')
return
res_s = alloc_s['result']
print(f' 解释方差 = {res_s["explained"]:.4f}')
print(' 注:S 分区的解释方差是「按头内 16 行取低秩主成分」算的,天然低于'
'全局 k-means(头内低秩只保留 ~49% 方差);但下面⑤部署掉点通常 <1pt,'
'说明被丢掉的方差对分类不敏感。')
ok = bool(np.all(res_s['assign'] == groups))
print(f' 归属是否严格等于结构边界(不混聚):{ok}')
res_g = nrfunc.regionify(W, signal='G', K=n_heads, order=1, r=4, seed=0)
mixed = {k: sorted(set((res_g['assign'] == k).nonzero()[0] // head_dim))
for k in range(n_heads)}
print(f' [对照·G 几何分区] 各区实际包含的头(应出现跨头混聚):{mixed}')
print('\n④ 生成物(量化打包 + 分片存储):')
show_artifacts(res_s, bits=8, n_shards=4)
recon = nrfunc.reconstruct(res_s)
with torch.no_grad():
model.blocks[0].wq.weight.copy_(torch.as_tensor(recon, dtype=torch.float32))
acc_after = accuracy(model, test_x4, test_y, dev)
print(f'\n⑤ 部署后 · 测试准确率 = {acc_after * 100:.2f}% '
f'(掉 {acc_before * 100 - acc_after * 100:.2f}pt)')
x_batch = np.random.default_rng(0).standard_normal(
(256, model.d_model)).astype(np.float32)
print('\n价值对比(wq 层 64×64,函数化前 vs 后):')
show_value(W, res_s, x_batch)
print('\n[GPT 级入口] regionify_transformer_block():')
try:
nrfunc.regionify_transformer_block({})
except NotImplementedError as e:
print(f' 触发 NotImplementedError:{e}')
print('\n[诚实声明] 本例用玩具 ViT 证明 S 结构分区「接口可跑 + 按头切正确 + '
'生成物可还原」;是否在真实大 Transformer(ViT/LLM)上保精度,尚未验证。'
'GPT 级大模型为「理论预演、不验证」,谁有条件谁验证。')
if __name__ == '__main__':
main()