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
- 实时五通道感知 + 具身闭环运行
This commit is contained in:
2026-07-07 09:19:31 +08:00
commit e782ef4db1
30 changed files with 7446 additions and 0 deletions

464
jspaceai/multimodal.py Normal file
View File

@@ -0,0 +1,464 @@
"""
多模态感知-行动系统
原生支持图片、音频、视频、文本四种模态。
所有模态编码到统一的 workspace 向量空间,从 workspace 解码到任意模态。
设计原则:
1. 原生多模态——不经过文本中间表示
2. 流式处理——支持实时麦克风/摄像头输入
3. 闭环——输出反馈到输入
4. 在线学习——持续进化
模态专家分工(对应 Anthropic 论文的"专家分工"
- 视觉专家:处理图片/视频帧
- 听觉专家:处理音频频谱
- 语言专家:处理文本 token
- 跨模态专家:对齐不同模态的概念
每个专家有自己的内部状态 m_i通过 J-space 在 workspace w 中广播。
"""
from __future__ import annotations
import torch
import torch.nn as nn
import torch.nn.functional as F
from dataclasses import dataclass, field
from typing import Optional
import numpy as np
from .core import JSpaceConfig, Expert, JSpaceWorkspace
@dataclass
class MultimodalConfig(JSpaceConfig):
"""多模态配置"""
# 文本
vocab_size: int = 100
embed_dim: int = 16
# 视觉
img_channels: int = 3
img_size: int = 32 # 编码后的图像尺寸(原始会 resize
visual_feature_dim: int = 32
# 音频
audio_sample_rate: int = 16000
audio_frame_size: int = 1024 # 每帧采样数
audio_feature_dim: int = 32
# 共享
workspace_dim: int = 64 # 比 unimodal 大,承载多模态
num_experts: int = 12 # 12 个专家2视觉 + 2屏幕 + 2听觉 + 2语言 + 2鼠标 + 2跨模态
expert_dim: int = 24
# 键盘/鼠标
keyboard_vocab: int = 128 # 键盘字符词汇表
mouse_feature_dim: int = 16 # 鼠标特征维度
class VisualEncoder(nn.Module):
"""
视觉编码器:图片 → workspace 向量
轻量 CNN不用预训练。从零学。
输入:(batch, 3, H, W)
输出:(batch, input_dim) 投影到 workspace 输入空间
"""
def __init__(self, input_dim: int):
super().__init__()
self.conv = nn.Sequential(
nn.Conv2d(3, 16, 3, stride=2, padding=1), # 32→16
nn.ReLU(),
nn.Conv2d(16, 32, 3, stride=2, padding=1), # 16→8
nn.ReLU(),
nn.Conv2d(32, 32, 3, stride=2, padding=1), # 8→4
nn.ReLU(),
nn.AdaptiveAvgPool2d(1), # 全局池化 → (32, 1, 1)
)
self.proj = nn.Linear(32, input_dim)
def forward(self, img: torch.Tensor) -> torch.Tensor:
"""
img: (batch, 3, H, W) 或 (batch, T, 3, H, W) 视频序列
returns: (batch, input_dim) 或 (batch, T, input_dim)
"""
if img.dim() == 5: # 视频序列
B, T, C, H, W = img.shape
img = img.reshape(B * T, C, H, W)
feat = self.conv(img).squeeze(-1).squeeze(-1) # (B*T, 32)
feat = self.proj(feat) # (B*T, input_dim)
return feat.reshape(B, T, -1)
else:
feat = self.conv(img).squeeze(-1).squeeze(-1)
return self.proj(feat)
class AudioEncoder(nn.Module):
"""
音频编码器:音频波形 → workspace 向量
轻量 1D CNN 处理原始波形。
输入:(batch, audio_frame_size) 原始音频采样
输出:(batch, input_dim)
"""
def __init__(self, input_dim: int, audio_frame_size: int = 1024):
super().__init__()
self.conv = nn.Sequential(
nn.Conv1d(1, 16, 64, stride=4, padding=32), # 下采样
nn.ReLU(),
nn.Conv1d(16, 32, 32, stride=4, padding=16),
nn.ReLU(),
nn.Conv1d(32, 32, 16, stride=2, padding=8),
nn.ReLU(),
nn.AdaptiveAvgPool1d(1),
)
self.proj = nn.Linear(32, input_dim)
def forward(self, audio: torch.Tensor) -> torch.Tensor:
"""
audio: (batch, frame_size) 或 (batch, T, frame_size)
returns: (batch, input_dim) 或 (batch, T, input_dim)
"""
if audio.dim() == 3:
B, T, F = audio.shape
audio = audio.reshape(B * T, F)
x = audio.unsqueeze(1) # (B*T, 1, F)
feat = self.conv(x).squeeze(-1)
feat = self.proj(feat)
return feat.reshape(B, T, -1)
else:
x = audio.unsqueeze(1) # (B, 1, F)
feat = self.conv(x).squeeze(-1)
return self.proj(feat)
class TextEncoder(nn.Module):
"""文本编码器token → workspace 向量"""
def __init__(self, vocab_size: int, embed_dim: int, input_dim: int):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.proj = nn.Linear(embed_dim, input_dim, bias=False)
def forward(self, token_ids: torch.Tensor) -> torch.Tensor:
"""token_ids: (batch,) 或 (batch, T)"""
if token_ids.dim() == 1:
emb = self.embedding(token_ids)
return self.proj(emb)
else:
emb = self.embedding(token_ids) # (B, T, embed)
return self.proj(emb)
class KeyboardEncoder(nn.Module):
"""
键盘编码器:按键序列 → workspace 向量
把键盘按键序列编码成语义向量。
和 TextEncoder 类似,但字符集不同(含特殊键)。
"""
def __init__(self, vocab_size: int, embed_dim: int, input_dim: int):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.proj = nn.Linear(embed_dim, input_dim, bias=False)
def forward(self, key_ids: torch.Tensor) -> torch.Tensor:
"""key_ids: (batch,) 或 (batch, T)"""
if key_ids.dim() == 1:
return self.proj(self.embedding(key_ids))
else:
return self.proj(self.embedding(key_ids))
class MouseEncoder(nn.Module):
"""
鼠标编码器:鼠标位置 + 事件 → workspace 向量
输入:(batch, 4) = (x_norm, y_norm, click_left, click_right)
x_norm, y_norm: 归一化到 [0, 1] 的鼠标坐标
click_left, click_right: 0 或 1
"""
def __init__(self, input_dim: int):
super().__init__()
self.net = nn.Sequential(
nn.Linear(4, 32),
nn.ReLU(),
nn.Linear(32, input_dim),
)
def forward(self, mouse_data: torch.Tensor) -> torch.Tensor:
"""mouse_data: (batch, 4) → (batch, input_dim)"""
return self.net(mouse_data)
class VisualDecoder(nn.Module):
"""视觉解码器workspace 向量 → 图片"""
def __init__(self, workspace_dim: int, img_size: int = 32):
super().__init__()
self.img_size = img_size
self.fc = nn.Linear(workspace_dim, 32 * 4 * 4)
self.deconv = nn.Sequential(
nn.ConvTranspose2d(32, 32, 3, stride=2, padding=1, output_padding=1), # 4→8
nn.ReLU(),
nn.ConvTranspose2d(32, 16, 3, stride=2, padding=1, output_padding=1), # 8→16
nn.ReLU(),
nn.ConvTranspose2d(16, 3, 3, stride=2, padding=1, output_padding=1), # 16→32
nn.Tanh(),
)
def forward(self, w: torch.Tensor) -> torch.Tensor:
"""w: (batch, workspace_dim) → (batch, 3, 32, 32)"""
x = self.fc(w).reshape(-1, 32, 4, 4)
return self.deconv(x)
class AudioDecoder(nn.Module):
"""音频解码器workspace 向量 → 音频波形"""
def __init__(self, workspace_dim: int, audio_frame_size: int = 1024):
super().__init__()
self.audio_frame_size = audio_frame_size
self.fc = nn.Linear(workspace_dim, 32 * 64)
self.deconv = nn.Sequential(
nn.ConvTranspose1d(32, 32, 32, stride=4, padding=14, output_padding=2),
nn.ReLU(),
nn.ConvTranspose1d(32, 16, 16, stride=4, padding=6, output_padding=2),
nn.ReLU(),
nn.ConvTranspose1d(16, 1, 64, stride=4, padding=30, output_padding=2),
nn.Tanh(),
)
def forward(self, w: torch.Tensor) -> torch.Tensor:
"""w: (batch, workspace_dim) → (batch, audio_frame_size)"""
x = self.fc(w).reshape(-1, 32, 64)
audio = self.deconv(x).squeeze(1) # (batch, L)
# 裁剪或填充到目标长度
if audio.shape[-1] > self.audio_frame_size:
audio = audio[:, :self.audio_frame_size]
else:
audio = F.pad(audio, (0, self.audio_frame_size - audio.shape[-1]))
return audio
class TextDecoder(nn.Module):
"""文本解码器workspace 向量 → logits"""
def __init__(self, workspace_dim: int, vocab_size: int):
super().__init__()
self.net = nn.Sequential(
nn.Linear(workspace_dim, 64),
nn.Tanh(),
nn.Linear(64, vocab_size),
)
def forward(self, w: torch.Tensor) -> torch.Tensor:
return self.net(w)
class MultimodalJSpaceModel(nn.Module):
"""
多模态 JSpace 模型。
12 个专家分工:
专家 0,1: 视觉(处理摄像头图片)
专家 2,3: 屏幕(处理屏幕截图)
专家 4,5: 听觉(处理音频帧)
专家 6,7: 语言(处理文本 token
专家 8,9: 鼠标(处理鼠标位置/点击)
专家 10,11: 跨模态(对齐不同模态的概念)
所有专家共享 workspace w通过 J-space 广播。
支持的输入模态:
'image' - 摄像头图片 (batch, 3, H, W)
'screen' - 屏幕截图 (batch, 3, H, W)(用视觉编码器)
'audio' - 音频帧 (batch, frame_size)
'text' - 文本 token (batch,)
'keyboard' - 键盘按键 (batch,) 或 (batch, T)
'mouse' - 鼠标数据 (batch, 4) = (x, y, click_l, click_r)
"""
def __init__(self, config: MultimodalConfig):
super().__init__()
self.config = config
# 编码器(各模态 → input_dim
self.visual_encoder = VisualEncoder(config.input_dim) # 摄像头 + 屏幕
self.audio_encoder = AudioEncoder(config.input_dim, config.audio_frame_size)
self.text_encoder = TextEncoder(config.vocab_size, config.embed_dim, config.input_dim)
self.keyboard_encoder = KeyboardEncoder(config.keyboard_vocab, config.embed_dim, config.input_dim)
self.mouse_encoder = MouseEncoder(config.input_dim)
# 解码器workspace → 各模态)
self.visual_decoder = VisualDecoder(config.workspace_dim, config.img_size)
self.audio_decoder = AudioDecoder(config.workspace_dim, config.audio_frame_size)
self.text_decoder = TextDecoder(config.workspace_dim, config.vocab_size)
# 专家池
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,
)
# 模态类型标记12 个专家的分工)
self.expert_modality = (
['visual'] * 2 + # 0,1: 摄像头
['screen'] * 2 + # 2,3: 屏幕
['audio'] * 2 + # 4,5: 听觉
['text'] * 2 + # 6,7: 语言
['mouse'] * 2 + # 8,9: 鼠标
['cross'] * 2 # 10,11: 跨模态
)[:config.num_experts]
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, x: torch.Tensor,
record_trajectory: bool = False) -> tuple[dict, list]:
"""
单步前向
Args:
state: {'w': ..., 'm': [...]}
x: (batch, input_dim) 已编码的输入(任意模态)
record_trajectory: 是否记录 w 轨迹
Returns:
new_state, w_trajectory (list)
"""
w = state['w']
ms = state['m']
cfg = self.config
w_trajectory = []
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())
new_state = {'w': w, 'm': ms}
return new_state, w_trajectory
def encode_modality(self, modality: str, data: torch.Tensor) -> torch.Tensor:
"""编码任意模态到 input_dim
支持的模态image, screen, audio, text, keyboard, mouse
- image/screen: 共用 visual_encoder都是 RGB 图像)
- keyboard: 键盘按键 id
- mouse: (batch, 4) = (x_norm, y_norm, click_left, click_right)
"""
if modality in ('image', 'screen'):
return self.visual_encoder(data)
elif modality == 'audio':
return self.audio_encoder(data)
elif modality == 'text':
return self.text_encoder(data)
elif modality == 'keyboard':
return self.keyboard_encoder(data)
elif modality == 'mouse':
return self.mouse_encoder(data)
else:
raise ValueError(f"Unknown modality: {modality}")
def decode_modality(self, modality: str, w: torch.Tensor) -> torch.Tensor:
"""从 workspace 解码到任意模态
screen 用 visual_decoder和 image 共享)
keyboard 用 text_decoder都是 token
"""
if modality in ('image', 'screen'):
return self.visual_decoder(w)
elif modality == 'audio':
return self.audio_decoder(w)
elif modality in ('text', 'keyboard'):
return self.text_decoder(w)
else:
raise ValueError(f"Unknown modality: {modality}")
def forward_multimodal(self, modality: str, data: torch.Tensor,
state: dict | None = None,
record_trajectory: bool = False) -> tuple[dict, dict]:
"""
多模态前向
Args:
modality: 'image' / 'audio' / 'text'
data: 模态原始数据
state: 初始状态
record_trajectory: 是否记录 w 轨迹
Returns:
outputs: {'w': workspace, 'logits/img/audio': 各模态解码}
info: {'alpha', 'w_norm', 'w_trajectory'}
"""
if state is None:
# 先编码确定 batch size
x = self.encode_modality(modality, data)
batch_size = x.shape[0]
device = x.device
state = self.init_state(batch_size, device)
else:
x = self.encode_modality(modality, data)
# 处理序列或单步
if x.dim() == 2: # 单步 (batch, input_dim)
state, w_traj = self.step(state, x, record_trajectory)
w = state['w']
else: # 序列 (batch, T, input_dim)
w_traj_all = []
for t in range(x.shape[1]):
state, w_traj = self.step(state, x[:, t], record_trajectory)
if record_trajectory:
w_traj_all.append(w_traj)
w = state['w']
# 解码到所有模态workspace 是模态无关的,可解码到任意模态)
outputs = {
'w': w,
'image': self.visual_decoder(w),
'audio': self.audio_decoder(w),
'text_logits': self.text_decoder(w),
}
info = {
'w_norm': w.norm(dim=-1),
}
if record_trajectory and w_traj_all:
info['w_trajectory'] = w_traj_all
return outputs, info