456 lines
16 KiB
Python
456 lines
16 KiB
Python
"""
|
||
语言版 JSpace 模型 + 自主进化机制
|
||
|
||
核心扩展:
|
||
1. JSpaceLanguageModel: 在 JSpaceModel 基础上加 token embedding + logit 输出
|
||
2. 在线学习:每个 forward 累积梯度并更新参数(边推理边学习)
|
||
3. EWC(Elastic Weight Consolidation):保护重要参数,防灾难性遗忘
|
||
4. ExperienceReplay:经验回放缓冲区
|
||
5. ExpertPlasticity:专家专业化追踪,新知识优先路由到"空闲"专家
|
||
|
||
数学形式:
|
||
|
||
专家动力学(同 core.py):
|
||
dm_i/dt = -∇U_i(m_i) + J_i · w + P_i_in · embed(x)
|
||
|
||
工作空间动力学(同 core.py):
|
||
τ_w · dw/dt = -w + Σ α_i · P_i_out(m_i)
|
||
|
||
输出:
|
||
logits = Q(w) # 从工作空间投影到词汇表
|
||
|
||
在线学习目标:
|
||
L = -log p(x_{t+1} | x_{0:t}) + λ_EWC · Σ_i F_i · (θ_i - θ*_i)²
|
||
|
||
EWC 中 F_i 是 Fisher 信息矩阵对角线,衡量参数重要性。
|
||
重要参数被"锚定"在旧值附近,新知识只能修改不重要的参数。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import torch
|
||
import torch.nn as nn
|
||
import torch.nn.functional as F
|
||
from dataclasses import dataclass, field
|
||
from collections import deque
|
||
import random
|
||
|
||
from .core import JSpaceConfig, Expert, JSpaceWorkspace
|
||
|
||
|
||
@dataclass
|
||
class LanguageConfig(JSpaceConfig):
|
||
"""语言模型配置,继承 JSpaceConfig"""
|
||
vocab_size: int = 100 # 词汇表大小
|
||
embed_dim: int = 16 # token embedding 维度(≠ input_dim,会投影)
|
||
# input_dim 仍用 JSpaceConfig 的,作为工作空间接收的输入维度
|
||
|
||
|
||
class JSpaceLanguageModel(nn.Module):
|
||
"""
|
||
语言版 JSpace 模型。
|
||
|
||
流程:
|
||
token → embedding → 投影到 input_dim → 喂入 JSpace 动力学
|
||
工作空间 w → 投影到 vocab_size → logits → 采样 token
|
||
|
||
每步可训练(在线学习):
|
||
forward 后用 cross-entropy loss 更新参数
|
||
"""
|
||
|
||
def __init__(self, config: LanguageConfig):
|
||
super().__init__()
|
||
self.config = config
|
||
|
||
# Token embedding
|
||
self.embedding = nn.Embedding(config.vocab_size, config.embed_dim)
|
||
# 投影 embedding → input_dim(JSpace 期望的输入维度)
|
||
self.input_proj = nn.Linear(config.embed_dim, config.input_dim, bias=False)
|
||
|
||
# 专家池
|
||
self.experts = nn.ModuleList([
|
||
Expert(
|
||
expert_dim=config.expert_dim,
|
||
workspace_dim=config.workspace_dim,
|
||
input_dim=config.input_dim,
|
||
num_wells=config.num_wells,
|
||
sparsity=config.jacobian_sparsity,
|
||
)
|
||
for _ in range(config.num_experts)
|
||
])
|
||
|
||
# 工作空间
|
||
self.workspace = JSpaceWorkspace(
|
||
workspace_dim=config.workspace_dim,
|
||
input_dim=config.input_dim,
|
||
num_experts=config.num_experts,
|
||
)
|
||
|
||
# 输出头:w → logits
|
||
self.output_head = nn.Sequential(
|
||
nn.Linear(config.workspace_dim, 64),
|
||
nn.Tanh(),
|
||
nn.Linear(64, config.vocab_size),
|
||
)
|
||
|
||
def init_state(self, batch_size: int, device: torch.device) -> dict:
|
||
return {
|
||
'w': torch.zeros(batch_size, self.config.workspace_dim, device=device),
|
||
'm': [torch.zeros(batch_size, self.config.expert_dim, device=device)
|
||
for _ in range(self.config.num_experts)],
|
||
}
|
||
|
||
def step(self, state: dict, token_ids: torch.Tensor,
|
||
record_trajectory: bool = False) -> tuple[dict, torch.Tensor, torch.Tensor, list]:
|
||
"""单时间步前向
|
||
|
||
Args:
|
||
state: {'w': ..., 'm': [...]}
|
||
token_ids: (batch,) token indices
|
||
record_trajectory: 是否记录 w 轨迹(J-lens 用)
|
||
|
||
Returns:
|
||
new_state, logits (batch, vocab_size), alpha (batch, num_experts),
|
||
w_trajectory (list of (batch, workspace_dim)) 或空 list
|
||
"""
|
||
w = state['w']
|
||
ms = state['m']
|
||
cfg = self.config
|
||
w_trajectory = []
|
||
|
||
# token → embedding → input projection
|
||
emb = self.embedding(token_ids) # (batch, embed_dim)
|
||
x = self.input_proj(emb) # (batch, input_dim)
|
||
|
||
# ODE 子步积分(对应 Anthropic 论文的"层")
|
||
for substep in range(cfg.ode_steps):
|
||
contributions = []
|
||
new_ms = []
|
||
for i, expert in enumerate(self.experts):
|
||
m_next, contrib = expert(
|
||
ms[i], w, x,
|
||
dt=cfg.dt, noise_std=cfg.noise_std,
|
||
)
|
||
new_ms.append(m_next)
|
||
contributions.append(contrib)
|
||
contributions = torch.stack(contributions, dim=1)
|
||
|
||
w, alpha = self.workspace(
|
||
w, x, contributions,
|
||
dt=cfg.dt, tau_w=cfg.tau_w,
|
||
)
|
||
ms = new_ms
|
||
|
||
if record_trajectory:
|
||
w_trajectory.append(w.detach())
|
||
|
||
logits = self.output_head(w) # (batch, vocab_size)
|
||
new_state = {'w': w, 'm': ms}
|
||
return new_state, logits, alpha, w_trajectory
|
||
|
||
def forward(self, token_seqs: torch.Tensor, state: dict | None = None,
|
||
record_trajectory: bool = False) -> tuple[torch.Tensor, dict]:
|
||
"""
|
||
Args:
|
||
token_seqs: (batch, T) token indices
|
||
record_trajectory: 是否记录 w 轨迹
|
||
|
||
Returns:
|
||
logits: (batch, T, vocab_size)
|
||
info: {'alpha': (batch, T, num_experts), 'w_norm': (batch, T),
|
||
'w_trajectory': list of (batch, T, workspace_dim) 或空}
|
||
"""
|
||
batch_size, T = token_seqs.shape
|
||
device = token_seqs.device
|
||
|
||
if state is None:
|
||
state = self.init_state(batch_size, device)
|
||
|
||
logits_list = []
|
||
alphas = []
|
||
w_norms = []
|
||
w_traj_per_step = [] # list of (list of substep w)
|
||
for t in range(T):
|
||
state, logits, alpha, w_traj = self.step(
|
||
state, token_seqs[:, t], record_trajectory=record_trajectory
|
||
)
|
||
logits_list.append(logits)
|
||
alphas.append(alpha)
|
||
w_norms.append(state['w'].norm(dim=-1))
|
||
if record_trajectory and w_traj:
|
||
w_traj_per_step.append(torch.stack(w_traj, dim=1)) # (batch, n_substeps, workspace_dim)
|
||
|
||
logits = torch.stack(logits_list, dim=1) # (batch, T, vocab_size)
|
||
info = {
|
||
'alpha': torch.stack(alphas, dim=1), # (batch, T, num_experts)
|
||
'w_norm': torch.stack(w_norms, dim=1), # (batch, T)
|
||
}
|
||
if record_trajectory and w_traj_per_step:
|
||
info['w_trajectory'] = torch.stack(w_traj_per_step, dim=1) # (batch, T, n_substeps, workspace_dim)
|
||
return logits, info
|
||
|
||
@torch.no_grad()
|
||
def generate(self, prompt: list[int], n_new: int = 50, temperature: float = 1.0,
|
||
top_k: int = 5) -> list[int]:
|
||
"""自回归生成
|
||
|
||
Args:
|
||
prompt: 起始 token ids
|
||
n_new: 生成的新 token 数
|
||
temperature: 采样温度
|
||
top_k: top-k 采样
|
||
"""
|
||
was_training = self.training
|
||
self.eval()
|
||
device = next(self.parameters()).device
|
||
state = self.init_state(1, device)
|
||
|
||
# 预热 state with prompt
|
||
tokens = list(prompt)
|
||
for tok in tokens:
|
||
state, _, _, _ = self.step(state, torch.tensor([tok], device=device))
|
||
|
||
# 生成
|
||
generated = []
|
||
for _ in range(n_new):
|
||
state, logits, _, _ = self.step(state, torch.tensor([tokens[-1]], device=device))
|
||
logits = logits[0] / max(temperature, 1e-6)
|
||
|
||
if top_k > 0:
|
||
top_k = min(top_k, logits.size(-1))
|
||
vals, idxs = logits.topk(top_k)
|
||
probs = F.softmax(vals, dim=-1)
|
||
next_tok = idxs[torch.multinomial(probs, 1)].item()
|
||
else:
|
||
probs = F.softmax(logits, dim=-1)
|
||
next_tok = torch.multinomial(probs, 1).item()
|
||
|
||
generated.append(next_tok)
|
||
tokens.append(next_tok)
|
||
|
||
if was_training:
|
||
self.train()
|
||
else:
|
||
self.eval()
|
||
return generated
|
||
|
||
|
||
class ExperienceReplay:
|
||
"""
|
||
经验回放缓冲区。
|
||
|
||
存储见过的序列片段,训练时随机采样混入当前 batch。
|
||
防止灾难性遗忘——旧知识被定期"复习"。
|
||
"""
|
||
|
||
def __init__(self, capacity: int = 1000, seq_len: int = 64):
|
||
self.capacity = capacity
|
||
self.seq_len = seq_len
|
||
self.buffer: deque = deque(maxlen=capacity)
|
||
|
||
def push(self, token_seq: torch.Tensor):
|
||
"""push 一个序列 (T,) 或 (batch, T)"""
|
||
if token_seq.dim() == 1:
|
||
token_seq = token_seq.unsqueeze(0)
|
||
for seq in token_seq:
|
||
if len(seq) >= self.seq_len:
|
||
self.buffer.append(seq.clone())
|
||
|
||
def sample(self, batch_size: int) -> torch.Tensor | None:
|
||
"""采样 (batch_size, seq_len)"""
|
||
if len(self.buffer) < batch_size:
|
||
return None
|
||
samples = random.sample(list(self.buffer), batch_size)
|
||
# 随机裁剪到 seq_len
|
||
result = []
|
||
for s in samples:
|
||
if len(s) > self.seq_len:
|
||
start = random.randint(0, len(s) - self.seq_len - 1)
|
||
result.append(s[start:start + self.seq_len])
|
||
else:
|
||
result.append(s)
|
||
return torch.stack(result)
|
||
|
||
|
||
class EWCOptimizer:
|
||
"""
|
||
Elastic Weight Consolidation 优化器包装。
|
||
|
||
核心思想:参数 θ 有"重要性" F(Fisher 信息)。
|
||
重要参数偏离原值 θ* 会被惩罚。
|
||
新知识只能修改不重要的参数。
|
||
|
||
L_total = L_task + λ · Σ_i F_i · (θ_i - θ*_i)²
|
||
|
||
工作流:
|
||
1. 正常训练一段时间
|
||
2. 调用 consolidate():计算 Fisher 信息,锚定当前参数
|
||
3. 继续训练——loss 中加入 EWC 正则
|
||
4. 周期性 consolidate(更新锚点和重要性)
|
||
"""
|
||
|
||
def __init__(self, model: nn.Module, lr: float = 1e-3,
|
||
ewc_lambda: float = 1.0, max_grad_norm: float = 1.0,
|
||
weight_decay: float = 0.0):
|
||
self.model = model
|
||
if weight_decay > 0:
|
||
self.optimizer = torch.optim.AdamW(
|
||
model.parameters(), lr=lr, weight_decay=weight_decay,
|
||
)
|
||
else:
|
||
self.optimizer = torch.optim.Adam(model.parameters(), lr=lr)
|
||
self.ewc_lambda = ewc_lambda
|
||
self.max_grad_norm = max_grad_norm
|
||
self.weight_decay = weight_decay
|
||
|
||
# Fisher 信息和锚定参数
|
||
self.fisher: dict[str, torch.Tensor] = {}
|
||
self.anchored_params: dict[str, torch.Tensor] = {}
|
||
|
||
def consolidate(self, data_sample: torch.Tensor, n_samples: int = 50):
|
||
"""计算 Fisher 信息并锚定当前参数
|
||
|
||
Args:
|
||
data_sample: (batch, T) 用于计算 Fisher 的数据样本
|
||
n_samples: 采样次数(Fisher 信息的 Monte Carlo 估计)
|
||
"""
|
||
# 保存当前参数作为锚点
|
||
self.anchored_params = {
|
||
name: param.data.clone()
|
||
for name, param in self.model.named_parameters()
|
||
}
|
||
|
||
# 计算 Fisher 信息(对角近似)
|
||
fisher = {
|
||
name: torch.zeros_like(param)
|
||
for name, param in self.model.named_parameters()
|
||
}
|
||
|
||
self.model.eval()
|
||
for _ in range(n_samples):
|
||
self.model.zero_grad()
|
||
logits, _ = self.model(data_sample)
|
||
# 只用 logits[:, :-1] 对应 targets[:, 1:] 的部分
|
||
logits_pred = logits[:, :-1] # (batch, T-1, vocab)
|
||
probs = F.softmax(logits_pred, dim=-1) # (batch, T-1, vocab)
|
||
# 采样 token 计算 Fisher
|
||
sampled_tokens = torch.multinomial(
|
||
probs.reshape(-1, probs.size(-1)), 1
|
||
).view_as(logits_pred[:, :, 0]) # (batch, T-1)
|
||
log_probs = F.log_softmax(logits_pred, dim=-1)
|
||
loss = -log_probs.gather(-1, sampled_tokens.unsqueeze(-1)).mean()
|
||
loss.backward()
|
||
|
||
for name, param in self.model.named_parameters():
|
||
if param.grad is not None:
|
||
fisher[name] += param.grad.data ** 2
|
||
|
||
# 平均
|
||
for name in fisher:
|
||
fisher[name] /= n_samples
|
||
|
||
self.fisher = fisher
|
||
self.model.zero_grad()
|
||
self.model.train()
|
||
|
||
def ewc_penalty(self) -> torch.Tensor:
|
||
"""计算 EWC 正则项"""
|
||
if not self.fisher:
|
||
return torch.tensor(0.0, device=next(self.model.parameters()).device)
|
||
|
||
penalty = 0.0
|
||
for name, param in self.model.named_parameters():
|
||
if name in self.fisher:
|
||
penalty = penalty + (self.fisher[name] * (param - self.anchored_params[name]) ** 2).sum()
|
||
return penalty
|
||
|
||
def step(self, loss: torch.Tensor):
|
||
"""一步优化:task loss + EWC 正则"""
|
||
total_loss = loss + self.ewc_lambda * self.ewc_penalty()
|
||
self.optimizer.zero_grad()
|
||
total_loss.backward()
|
||
torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=self.max_grad_norm)
|
||
self.optimizer.step()
|
||
return total_loss.item()
|
||
|
||
def state_dict(self) -> dict:
|
||
return {
|
||
"optimizer": self.optimizer.state_dict(),
|
||
"ewc_lambda": self.ewc_lambda,
|
||
"max_grad_norm": self.max_grad_norm,
|
||
"weight_decay": self.weight_decay,
|
||
"fisher": self.fisher,
|
||
"anchored_params": self.anchored_params,
|
||
}
|
||
|
||
def load_state_dict(self, state: dict):
|
||
if "optimizer" in state:
|
||
self.optimizer.load_state_dict(state["optimizer"])
|
||
self.ewc_lambda = state.get("ewc_lambda", self.ewc_lambda)
|
||
self.max_grad_norm = state.get("max_grad_norm", self.max_grad_norm)
|
||
self.weight_decay = state.get("weight_decay", self.weight_decay)
|
||
self.fisher = state.get("fisher", {})
|
||
self.anchored_params = state.get("anchored_params", {})
|
||
|
||
|
||
class ExpertPlasticity:
|
||
"""
|
||
专家结构可塑性追踪。
|
||
|
||
追踪每个专家的"专业化程度":
|
||
- 哪些专家在处理哪些模式
|
||
- 哪些专家"负载过重"(应该分裂或新增)
|
||
- 哪些专家"空闲"(可以接收新知识)
|
||
|
||
这不是真正的动态增删专家(实现复杂),而是:
|
||
- 统计专家使用率
|
||
- 在路由时给空闲专家加权(鼓励新知识流向空闲专家)
|
||
"""
|
||
|
||
def __init__(self, num_experts: int, ema_alpha: float = 0.99):
|
||
self.num_experts = num_experts
|
||
self.ema_alpha = ema_alpha
|
||
# 每个专家的使用率(EMA)
|
||
self.usage = torch.ones(num_experts) / num_experts
|
||
# 每个专家的"领地"——它擅长的 token 分布
|
||
self.expert_specialization: list[dict[int, float]] = [{} for _ in range(num_experts)]
|
||
|
||
def update(self, alpha: torch.Tensor, tokens: torch.Tensor):
|
||
"""更新专家统计
|
||
|
||
Args:
|
||
alpha: (batch, T, num_experts) 注意力权重
|
||
tokens: (batch, T) 对应的 token
|
||
"""
|
||
# 使用率(时间维度平均)
|
||
usage_batch = alpha.mean(dim=(0, 1)).detach().cpu() # (num_experts,)
|
||
self.usage = self.ema_alpha * self.usage + (1 - self.ema_alpha) * usage_batch
|
||
|
||
# 专业化:每个专家最常处理哪些 token
|
||
alpha_flat = alpha.reshape(-1, self.num_experts).detach().cpu() # (batch*T, num_experts)
|
||
tokens_flat = tokens.reshape(-1).detach().cpu().tolist()
|
||
for tok, weights in zip(tokens_flat, alpha_flat):
|
||
for i, w in enumerate(weights.tolist()):
|
||
if w > 0.1: # 只记录显著激活
|
||
self.expert_specialization[i][tok] = \
|
||
self.expert_specialization[i].get(tok, 0) + w
|
||
|
||
def get_diversity_bonus(self) -> torch.Tensor:
|
||
"""返回多样性奖励——给使用率低的专家加权
|
||
|
||
在路由注意力上加上这个 bonus,鼓励新知识流向空闲专家
|
||
"""
|
||
# 使用率越低,bonus 越高
|
||
bonus = (1.0 - self.usage) / self.usage.clamp(min=1e-4)
|
||
bonus = bonus / bonus.sum() # 归一化
|
||
return bonus
|
||
|
||
def get_stats(self) -> dict:
|
||
"""返回可解释性统计"""
|
||
return {
|
||
'usage': self.usage.tolist(),
|
||
'top_specialization': [
|
||
sorted(s.items(), key=lambda x: -x[1])[:5]
|
||
for s in self.expert_specialization
|
||
],
|
||
}
|