Files
JspaceAI/jspaceai/language_model.py
XiuchengWu e782ef4db1 feat: JspaceAI 自主智慧架构
基于第一性原理 + Anthropic 2026 J-space 论文实现的具身智慧系统。

核心架构:
- ODE 动力系统 + 并行专家 + J-space 工作空间广播
- 12 个异构专家(视觉/屏幕/听觉/语言/鼠标/跨模态)
- workspace 256 维 + LayerNorm + RK4 积分

自主心智(最重要的能力):
- 好奇心驱动探索(内在奖励 + 世界模型)
- 跨会话状态持久化(海洋不蒸发)
- 自我模型(知道自己会什么不会什么)
- 元学习(自适应学习率 + 策略选择)

具身 Agent(完整神经系统):
- 感知层:摄像头 + 麦克风 + 屏幕 + 键盘 + 鼠标
- 大脑皮层(workspace)+ 小脑(运动控制)+ 中枢神经(门控)
- 海马体(情景记忆)+ 基底神经节(动作选择)
- 执行器:鼠标控制 + 键盘输出 + 音频播放 + 屏幕绘制

多模态支持:
- 原生图像/音频/视频/文本/键盘/鼠标 6 种模态
- 跨平台(macOS/Windows/Linux)

外挂模块系统:
- 可热插拔的外部能力(小模型/知识库/工具)
- 核心心智不依赖外挂,断开后继续工作

守护进程:
- 用户主动 start/stop(不自启)
- 后台静默运行,持续感知学习
- 状态自动保存,跨会话继续

J-lens 可解释性:
- 观测模型内部每个 ODE 子步的想法
- Directed Modulation 验证 workspace 因果作用
- Selectivity 验证(ablate workspace)

小模型蒸馏:
- 接 GPT-2/Qwen 等迁移理解能力
- 蒸馏完成后小模型可断开

验证结果:
- 连续序列:JSpace 胜 Flat 39.7%
- 语言进化:loss 3.95→2.40
- workspace ||w||:v1 0.05 → v2 16.0
- 实时五通道感知 + 具身闭环运行
2026-07-07 09:20:15 +08:00

426 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
语言版 JSpace 模型 + 自主进化机制
核心扩展:
1. JSpaceLanguageModel: 在 JSpaceModel 基础上加 token embedding + logit 输出
2. 在线学习:每个 forward 累积梯度并更新参数(边推理边学习)
3. EWCElastic 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_dimJSpace 期望的输入维度)
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 采样
"""
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)
self.train()
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 优化器包装。
核心思想:参数 θ 有"重要性" FFisher 信息)。
重要参数偏离原值 θ* 会被惩罚。
新知识只能修改不重要的参数。
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):
self.model = model
self.optimizer = torch.optim.Adam(model.parameters(), lr=lr)
self.ewc_lambda = ewc_lambda
self.max_grad_norm = max_grad_norm
# 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()
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
],
}