Refactor JspaceAI codebase: remove main_multimodal.py and main_v2.py, add main_chat.py for interactive dialogue mode with online learning capabilities.

This commit is contained in:
2026-07-07 11:22:53 +08:00
parent e782ef4db1
commit d5affe4458
15 changed files with 651 additions and 2795 deletions

397
main.py
View File

@@ -1,245 +1,242 @@
#!/usr/bin/env python3
"""
JspaceAI —— 主程序入口
JspaceAI —— 全部接入版(具身 Agent + 完整神经系统 + 自主心智)
对比实验:
1. JSpaceModel工作空间 + J-space 广播 + ODE 动力学)
2. FlatBaseline同样参数量的扁平 MLP
接入全部 5 个感官输入 + 4 个输出执行器 + 神经系统(小脑/海马体/基底神经节/中枢神经)
+ 自主心智(好奇心驱动 + 状态持久化 + 自我模型 + 元学习)。
任务多模态连续时间序列预测4 种模式切换)
输出:
- 训练/评估 loss 曲线对比
- 注意力热力图(哪个专家在哪个时段被激活)
- 工作空间 ||w|| 演化曲线(输出门控信号)
- 预测 vs 真实序列对比
模式:
--mode test: 子系统自检(权限 + I/O + 模型 + 海马体回忆)
--mode live: 实时具身循环 + 自主心智(默认安全:不控制鼠标键盘)
--mode safe: 同 live 但更保守(更高动作门控阈值)
运行:
python main.py
python main.py --steps 1000 --device cuda
python main.py --mode test
python main.py --mode live --steps 100
python main.py --mode safe --steps 50
python main.py --mode live --steps 200 --unsafe # 允许鼠标键盘输出
再次运行会从上次状态继续(海洋不蒸发)。
"""
from __future__ import annotations
import argparse
import torch
import numpy as np
import matplotlib.pyplot as plt
from pathlib import Path
import time
from jspaceai import (
JSpaceConfig, JSpaceModel, FlatBaseline,
ContinuousSequenceTask, Trainer,
MultimodalConfig, MultimodalJSpaceModel, EmbodiedAgent,
AutonomousMind, PLATFORM,
get_screen_size, print_permission_guide,
check_camera_permission, check_microphone_permission,
check_input_monitoring_permission,
)
def count_params(model: torch.nn.Module) -> int:
return sum(p.numel() for p in model.parameters() if p.requires_grad)
def run_experiment(n_steps: int, device: str, outdir: Path):
outdir.mkdir(parents=True, exist_ok=True)
torch.manual_seed(42)
np.random.seed(42)
# 任务
task = ContinuousSequenceTask(input_dim=8, seq_len=64, seed=42)
# 模型 1: JSpaceModel
config = JSpaceConfig(
input_dim=8,
workspace_dim=32,
expert_dim=16,
num_experts=5,
num_wells=4,
ode_steps=4,
dt=0.1,
tau_w=0.3,
output_threshold=0.5,
jacobian_sparsity=8,
noise_std=0.01,
def get_config() -> MultimodalConfig:
return MultimodalConfig(
vocab_size=50, embed_dim=16, input_dim=8, workspace_dim=64,
expert_dim=24, num_experts=12, num_wells=4, ode_steps=3,
dt=0.1, tau_w=0.3, jacobian_sparsity=16, noise_std=0.01,
img_size=32, audio_frame_size=1024, keyboard_vocab=128,
)
jspace_model = JSpaceModel(config)
# 模型 2: FlatBaseline参数量对齐
# JSpaceModel 大约的参数量:
# 专家: 5 × (4×16 + 4 + 16×8 + 16×32 + 16×32) ≈ 5 × 1108 = 5540
# 工作空间: (8+32)×32 + 32 + 32×32 + 32 ≈ 2144
# 预测头: 32×32 + 32 + 32×8 + 8 ≈ 1384
# 总计 ≈ 9000
flat_model = FlatBaseline(input_dim=8, hidden_dim=90, num_layers=2)
def test_subsystems():
"""子系统自检"""
print("=" * 60)
print("JspaceAI 对比实验")
print(f"具身 Agent 子系统测试 | 平台: {PLATFORM}")
print("=" * 60)
print(f"任务: 多模态连续序列预测 (input_dim=8, seq_len=64)")
print(f"训练步数: {n_steps}")
print(f"设备: {device}")
print()
print(f"JSpaceModel 参数量: {count_params(jspace_model):,}")
print(f"FlatBaseline 参数量: {count_params(flat_model):,}")
print()
# 训练
print("-" * 60)
print("训练 JSpaceModel...")
trainer_js = Trainer(jspace_model, task, lr=1e-3, device=device)
history_js = trainer_js.train(n_steps=n_steps, batch_size=32, eval_interval=n_steps // 10)
print("\n1. 权限检查:")
checks = {
'摄像头': check_camera_permission(),
'麦克风': check_microphone_permission(),
'键盘/鼠标监听': check_input_monitoring_permission(),
}
for name, ok in checks.items():
print(f" {name}: {'OK' if ok else '需要权限'}")
if not all(checks.values()):
print_permission_guide()
print()
print("-" * 60)
print("训练 FlatBaseline...")
trainer_flat = Trainer(flat_model, task, lr=1e-3, device=device)
history_flat = trainer_flat.train(n_steps=n_steps, batch_size=32, eval_interval=n_steps // 10)
sw, sh = get_screen_size()
print(f" 屏幕尺寸: {sw}x{sh}")
# 评估
print()
print("\n2. 模型 + Agent 初始化:")
config = get_config()
model = MultimodalJSpaceModel(config)
print(f" 模型参数: {sum(p.numel() for p in model.parameters()):,}")
print(f" 专家分工: {model.expert_modality}")
agent = EmbodiedAgent(
model, device='cpu',
enable_mouse_output=False, enable_keyboard_output=False,
enable_audio_output=True, enable_screen_output=False,
)
print(f" 小脑参数: {sum(p.numel() for p in agent.cerebellum.parameters()):,}")
print(f" 海马体容量: {agent.hippocampus.capacity}")
print(f" 反射弧数: {len(agent.cns.reflexes)}")
print("\n3. 单步循环测试2秒采集:")
agent.senses.start()
time.sleep(2)
for _ in range(5):
info = agent.step_once()
print(f" step {info['step']:2d} | mod {info['modality']:8s} | "
f"||w|| {info['w_norm']:.3f} | action {info['action']['action_idx']} | "
f"executed {info['action']['executed']} | mem {info['memories_count']}")
time.sleep(0.5)
agent.senses.stop()
agent.audio_actuator.stop()
print("\n4. 海马体回忆测试:")
if agent.hippocampus and agent.hippocampus.size() > 0:
w = agent.state['w'][0].cpu().numpy()
for i, m in enumerate(agent.hippocampus.recall(w, top_k=3)):
print(f" 记忆 {i}: sim={m['similarity']:.3f} ctx={m['context']}")
print("\n所有子系统测试完成")
def live(n_steps: int, device: str, safe_mode: bool = False, unsafe: bool = False):
"""实时具身循环 + 自主心智"""
print(f"平台: {PLATFORM}")
sw, sh = get_screen_size()
print(f"屏幕: {sw}x{sh}")
allow_output = unsafe and not safe_mode
if safe_mode:
print("\n安全模式:不执行鼠标/键盘动作")
print_permission_guide()
elif not allow_output:
print("\n默认模式:仅感知 + 音频/屏幕输出(不控制鼠标键盘)")
print(" 如需鼠标键盘输出,加 --unsafe")
config = get_config()
model = MultimodalJSpaceModel(config).to(device)
mp = Path('outputs/multimodal_model.pt')
if mp.exists():
try:
ckpt = torch.load(mp, map_location=device, weights_only=False)
model.load_state_dict(ckpt['model'], strict=False)
print(f"已加载模型: {mp}")
except Exception:
print("模型加载失败,随机初始化")
else:
print("未找到训练模型,随机初始化")
model.eval()
agent = EmbodiedAgent(
model, device=device,
enable_mouse_output=allow_output,
enable_keyboard_output=allow_output,
enable_audio_output=True,
enable_screen_output=not safe_mode,
risk_threshold=0.5 if safe_mode else 0.3,
)
mind = AutonomousMind(agent, save_dir='outputs/mind', device=device)
print("\n" + "=" * 60)
print("自主心智 - 全感官具身循环")
print("=" * 60)
print("最终评估")
print("=" * 60)
eval_xs = task.generate_batch(128)
eval_loss_js = trainer_js.evaluate(eval_xs)
eval_loss_flat = trainer_flat.evaluate(eval_xs)
print(f"JSpaceModel eval MSE: {eval_loss_js:.6f}")
print(f"FlatBaseline eval MSE: {eval_loss_flat:.6f}")
winner = "JSpaceModel" if eval_loss_js < eval_loss_flat else "FlatBaseline"
improvement = abs(eval_loss_js - eval_loss_flat) / max(eval_loss_js, eval_loss_flat) * 100
print(f"胜者: {winner} (相对优势 {improvement:.1f}%)")
print("好奇心驱动 + 状态持久化 + 自我模型 + 元学习")
print(f"运行 {n_steps}Ctrl+C 中断,状态自动保存)\n")
log = []
def on_step(info):
log.append(info)
if info['step'] % 10 == 0:
print(f" step {info['step']:4d} | mod {info['modality']:8s} | "
f"||w|| {info['w_norm']:.3f} | curio {info['curiosity']:.3f} | "
f"success {info['success']:.2f} | weak={info['weakness']} | "
f"mem {info['memory_count']}")
mind.run(n_steps=n_steps, interval=0.2, save_every=30, on_step=on_step)
print("\n" + mind.introspect())
# 可视化
print()
print("生成可视化...")
if log:
fig, axes = plt.subplots(2, 3, figsize=(18, 10))
steps = [s['step'] for s in log]
# 1. Loss 曲线对比
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# ||w|| 按模态着色
ax = axes[0, 0]
wn = [s['w_norm'] for s in log]
mods = [s['modality'] for s in log]
cm = {'image': 'green', 'screen': 'purple', 'audio': 'blue',
'keyboard': 'orange', 'mouse': 'red', 'idle': 'gray'}
colors = [cm.get(m, 'gray') for m in mods]
ax.scatter(steps, wn, c=colors, alpha=0.7, s=30)
ax.set_xlabel('Step'); ax.set_ylabel('||w||')
ax.set_title('Workspace Norm (by modality)'); ax.grid(True, alpha=0.3)
from matplotlib.patches import Patch
ax.legend(handles=[Patch(facecolor=c, label=m) for m, c in cm.items()],
fontsize=7)
ax = axes[0, 0]
ax.plot(history_js, label='JSpaceModel', alpha=0.7, linewidth=0.8)
ax.plot(history_flat, label='FlatBaseline', alpha=0.7, linewidth=0.8)
# 平滑曲线
if len(history_js) > 20:
smooth_js = np.convolve(history_js, np.ones(20)/20, mode='valid')
smooth_flat = np.convolve(history_flat, np.ones(20)/20, mode='valid')
ax.plot(smooth_js, label='JSpace (smoothed)', linewidth=2)
ax.plot(smooth_flat, label='Flat (smoothed)', linewidth=2)
ax.set_xlabel('Step')
ax.set_ylabel('MSE Loss')
ax.set_title('Training Loss')
ax.legend()
ax.set_yscale('log')
ax.grid(True, alpha=0.3)
# 2. 注意力热力图
test_xs = task.generate_batch(1) # 单条序列
alpha = trainer_js.get_attention(test_xs) # (1, T, num_experts)
if alpha is not None:
# 好奇心 + 成功度
ax = axes[0, 1]
alpha_np = alpha[0].cpu().numpy() # (T, num_experts)
im = ax.imshow(alpha_np.T, aspect='auto', cmap='hot', interpolation='nearest')
ax.set_xlabel('Time step')
ax.set_ylabel('Expert index')
ax.set_title('J-space Attention (which expert is active)')
plt.colorbar(im, ax=ax)
# 标注时段边界
for i in range(1, 64 // 16):
ax.axvline(x=i * 16, color='cyan', linestyle='--', alpha=0.7)
ax.set_xticks(range(0, 64, 16))
ax.plot(steps, [s['curiosity'] for s in log], 'r-', label='curiosity')
ax.plot(steps, [s['success'] for s in log], 'g-', label='success')
ax.legend(); ax.set_title('Curiosity & Success')
ax.grid(True, alpha=0.3)
# 3. 工作空间 ||w|| 演化
jspace_model.eval()
with torch.no_grad():
xs = test_xs.to(device)
_, info = jspace_model(xs)
w_norm = info['w_norm'][0].cpu().numpy()
# 模态分布
ax = axes[0, 2]
mc = {}
for m in mods:
mc[m] = mc.get(m, 0) + 1
ax.bar(mc.keys(), mc.values(),
color=[cm.get(m, 'gray') for m in mc.keys()])
ax.set_title('Modality Distribution')
ax = axes[1, 0]
ax.plot(w_norm, label='||w||', linewidth=2)
ax.axhline(y=config.output_threshold, color='r', linestyle='--',
label=f'threshold={config.output_threshold}', alpha=0.7)
ax.set_xlabel('Time step')
ax.set_ylabel('||w||')
ax.set_title('Workspace norm (output gating signal)')
ax.legend()
ax.grid(True, alpha=0.3)
for i in range(1, 64 // 16):
ax.axvline(x=i * 16, color='gray', linestyle='--', alpha=0.5)
# 自我模型
ax = axes[1, 0]
fc = log[-1]['self_confidence']
ax.barh(list(fc.keys()), list(fc.values()),
color=plt.cm.RdYlGn(list(fc.values())))
ax.set_xlim(0, 1); ax.set_title('Self Model (confidence)')
# 4. 预测 vs 真实(取第 0 维)
ax = axes[1, 1]
with torch.no_grad():
xs = test_xs.to(device)
preds_js, _ = jspace_model(xs)
preds_flat, _ = flat_model(xs)
true_seq = test_xs[0, 1:, 0].numpy()
pred_js = preds_js[0, :-1, 0].cpu().numpy()
pred_flat = preds_flat[0, :-1, 0].cpu().numpy()
# 世界模型 loss
ax = axes[1, 1]
ax.plot(steps, [s['world_loss'] for s in log], 'orange')
ax.set_title('World Model Loss'); ax.grid(True, alpha=0.3)
ax.plot(true_seq, label='True', linewidth=2, color='black')
ax.plot(pred_js, label='JSpace', alpha=0.8)
ax.plot(pred_flat, label='Flat', alpha=0.8)
ax.set_xlabel('Time step')
ax.set_ylabel('Value (dim 0)')
ax.set_title('Prediction vs True (dim 0)')
ax.legend()
ax.grid(True, alpha=0.3)
for i in range(1, 64 // 16):
ax.axvline(x=i * 16, color='gray', linestyle='--', alpha=0.3)
# 记忆数
ax = axes[1, 2]
ax.plot(steps, [s['memory_count'] for s in log], 'teal')
ax.set_title('Hippocampus Memory Count'); ax.grid(True, alpha=0.3)
plt.tight_layout()
fig_path = outdir / 'experiment.png'
plt.savefig(fig_path, dpi=150, bbox_inches='tight')
print(f"可视化已保存: {fig_path}")
plt.close()
# 保存模型
torch.save({
'jspace_model': jspace_model.state_dict(),
'flat_model': flat_model.state_dict(),
'config': config,
'eval_loss_js': eval_loss_js,
'eval_loss_flat': eval_loss_flat,
}, outdir / 'models.pt')
print()
print("=" * 60)
print("实验完成")
print("=" * 60)
print(f"JSpaceModel MSE: {eval_loss_js:.6f}")
print(f"FlatBaseline MSE: {eval_loss_flat:.6f}")
print(f"胜者: {winner}")
print()
print("关键观察点(看 experiment.png:")
print(" 1. Loss 曲线: JSpace 是否收敛更快/更低?")
print(" 2. 注意力热力图: 不同时段是否激活不同专家?")
print(" 3. ||w|| 曲线: 模式切换时是否有明显尖峰?")
print(" 4. 预测对比: 模式切换处哪个模型更鲁棒?")
plt.tight_layout()
Path('outputs').mkdir(exist_ok=True)
plt.savefig('outputs/embodied_live.png', dpi=150, bbox_inches='tight')
print(f"\n可视化: outputs/embodied_live.png")
def main():
parser = argparse.ArgumentParser(description='JspaceAI 对比实验')
parser.add_argument('--steps', type=int, default=500,
help='训练步数 (default: 500)')
parser.add_argument('--device', type=str, default='cpu',
help='设备 (cpu / cuda / mps)')
parser.add_argument('--outdir', type=str, default='outputs',
help='输出目录')
args = parser.parse_args()
p = argparse.ArgumentParser(description='JspaceAI 全部接入版(具身 + 自主心智)')
p.add_argument('--mode', default='test', choices=['test', 'live', 'safe'],
help='运行模式: test=自检, live=实时循环, safe=安全模式')
p.add_argument('--steps', type=int, default=100, help='live/safe 模式步数')
p.add_argument('--device', default='cpu', help='设备 (cpu/cuda/mps/auto)')
p.add_argument('--unsafe', action='store_true',
help='允许鼠标键盘输出(默认禁用)')
args = p.parse_args()
# 自动检测设备
if args.device == 'auto':
if torch.cuda.is_available():
device = 'cuda'
elif torch.backends.mps.is_available():
device = 'mps'
else:
device = 'cpu'
else:
device = args.device
dev = args.device
if dev == 'auto':
dev = 'cuda' if torch.cuda.is_available() else (
'mps' if torch.backends.mps.is_available() else 'cpu')
run_experiment(
n_steps=args.steps,
device=device,
outdir=Path(args.outdir),
)
if args.mode == 'test':
test_subsystems()
elif args.mode == 'live':
live(args.steps, dev, safe_mode=False, unsafe=args.unsafe)
elif args.mode == 'safe':
live(args.steps, dev, safe_mode=True, unsafe=False)
if __name__ == '__main__':