swarm-chat / src /core /brain.py
lk080424's picture
Upload src/core/brain.py with huggingface_hub
7b9033f verified
Raw
History Blame Contribute Delete
20.1 kB
#!/usr/bin/env python3
"""
虫群Brain v2.0 — AreaNetwork适配层
包装Meta Model的AreaNetwork(6区×3柱=18柱),补充chat/stats/get_area_weights等接口
让node_server和chat_engine无需修改即可使用完整版Meta Model引擎
"""
import numpy as np
import base64
import io
import os
from typing import Dict, Optional
from .functional_area import AreaNetwork, AreaType
from .language_decoder import LanguageDecoder
class Brain:
"""
虫群Brain v2.0 — AreaNetwork适配器
支持两种推理模式:
- 原始模式: 全链路forward(6区随机权重, 信号衰减严重)
- 映射模式: W_s(sensory直接映射) + P(motor投影), 跳过随机中间层
映射模式在训练后自动启用
"""
def __init__(self, dim: int = 75, config: dict = None):
self.dim = dim
self._config = config # 保存配置供后续使用
# AreaNetwork需要 {'sensory': {...}, ...} 格式
area_config = config.get('areas', config) if config else None
self._net = AreaNetwork(config=area_config)
self._forward_count = 0
# 默认冻结: forward只做推理,learn/train时临时解冻
self._net.freeze()
# 训练映射矩阵(加载后启用映射模式)
self._W_s = None # (75, 300) sensory直接映射
self._P = None # (300, 75) motor投影
self._load_mappings()
# QA检索索引(训练数据的问题向量→答案)
self._qa_vecs = None # (N, 75) 问题编码向量
self._qa_answers = [] # 对应答案列表
self._load_qa_index()
# 语言解码层 — motor输出→自回归token生成
self._decoder = None
self._init_decoder()
def _init_decoder(self):
"""初始化语言解码层,加载词表和已训练权重"""
try:
vocab_path = os.path.join(os.path.dirname(__file__), '..', '..', 'models', 'vocab75_clean_v2.pkl')
# 尝试json格式(优先)
json_path = vocab_path.replace('.pkl', '.json')
chars = None
if os.path.exists(json_path):
import json as _json
with open(json_path, 'r', encoding='utf-8') as f:
vocab_data = _json.load(f)
chars = list(vocab_data) if isinstance(vocab_data, list) else list(vocab_data.keys())
elif os.path.exists(vocab_path):
import pickle
with open(vocab_path, 'rb') as f:
vocab_data = pickle.load(f)
chars = list(vocab_data.keys()) if isinstance(vocab_data, dict) else list(vocab_data)
else:
# 尝试clean词表
alt_path = vocab_path.replace('vocab75_clean_v2', 'vocab75_clean')
alt_json = alt_path.replace('.pkl', '_words.json')
if os.path.exists(alt_json):
import json as _json
with open(alt_json, 'r', encoding='utf-8') as f:
chars = _json.load(f)
elif os.path.exists(alt_path.replace('.pkl', '.npz')):
# 从npz加载但需要词表
pass
if chars is None:
print('[Brain] 未找到词表,跳过解码层初始化')
self._decoder = None
return
motor_dim = self.config.get('motor_output_dim', 300)
hidden_dim = self.config.get('areas', {}).get('motor', {}).get('hidden_dim', 300)
self._decoder = LanguageDecoder(motor_dim=motor_dim, hidden_dim=hidden_dim, vocab_size=len(chars) + 3)
self._decoder.set_vocab(chars)
# 尝试加载已训练权重
dec_path = os.path.join(os.path.dirname(__file__), '..', '..', 'models', 'decoder_weights.npz')
if os.path.exists(dec_path):
self._decoder.load(dec_path)
print(f"[Brain] 语言解码层已加载训练权重")
else:
print(f"[Brain] 语言解码层已初始化(vocab={len(chars)}字, 未训练)")
except Exception as e:
print(f"[Brain] 语言解码层初始化失败: {e}")
self._decoder = None
@property
def areas(self) -> Dict:
"""兼容旧接口: 返回 {区名: FunctionalArea}"""
return {at.value: area for at, area in self._net.areas.items()}
def _load_mappings(self):
"""加载训练映射矩阵(W_s和P), 存在则启用映射模式"""
try:
import os
path = os.path.join(os.path.dirname(__file__), '..', '..', 'models', 'trained_mappings.npz')
if os.path.exists(path):
data = np.load(path, allow_pickle=True)
self._W_s = data['W_sensory'] # (75, 300)
self._P = data['P_motor'] # (300, 75)
print(f"[Brain] 映射模式已启用: W_s={self._W_s.shape}, P={self._P.shape}")
except Exception as e:
print(f"[Brain] 映射矩阵未找到, 使用原始模式: {e}")
def _load_qa_index(self):
"""加载QA检索索引: 对训练数据的问题编码,建立向量检索"""
import os, json
try:
qa_path = os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'qa_training.json')
if not os.path.exists(qa_path):
print(f"[Brain] QA训练数据未找到: {qa_path}")
return
with open(qa_path, 'r') as f:
qa_data = json.load(f)
from .semantic_encoder import get_encoder
encoder = get_encoder()
vecs, answers = [], []
for item in qa_data:
q = item.get('q') or item.get('question', '')
a = item.get('a') or item.get('answer', '')
if not q or not a:
continue
v = encoder.encode(q)
if v is not None and np.linalg.norm(v) > 0:
vecs.append(v[:self.dim])
answers.append(a)
if vecs:
self._qa_vecs = np.array(vecs, dtype=np.float32)
self._qa_answers = answers
print(f"[Brain] QA索引已加载: {len(answers)}条, vecs={self._qa_vecs.shape}")
except Exception as e:
print(f"[Brain] QA索引加载失败: {e}")
def _qa_retrieve(self, input_vec, threshold=0.75):
"""用输入向量检索最匹配的QA对"""
if self._qa_vecs is None or len(self._qa_answers) == 0:
return None, 0.0
v = input_vec[:self.dim]
v_norm = np.linalg.norm(v)
if v_norm < 1e-8:
return None, 0.0
# 批量cosine
norms = np.linalg.norm(self._qa_vecs, axis=1, keepdims=True)
norms = np.maximum(norms, 1e-8)
sims = (self._qa_vecs @ v) / (norms.ravel() * v_norm)
best_idx = int(np.argmax(sims))
best_sim = float(sims[best_idx])
if best_sim >= threshold:
return self._qa_answers[best_idx], best_sim
return None, best_sim
def forward(self, input_vec) -> Dict[str, np.ndarray]:
"""
前向传播 — 6区协作推理
映射模式: 输入75维→W_s(75×300)→sensory→AreaNetwork全链路→motor(300)→P(300×75)→输出75维
原始模式: 输入75维→AreaNetwork全链路→motor(300)
"""
if isinstance(input_vec, str):
return self.chat(input_vec)
x = np.asarray(input_vec, dtype=np.float32).ravel()[:self.dim]
if len(x) < self.dim:
x = np.pad(x, (0, self.dim - len(x)))
# AreaNetwork原始forward
output, meta = self._net.forward(x)
self._forward_count += 1
# 从meta提取各区输出
result = {}
area_outputs = meta.get('area_outputs', {})
for area_type, area_data in area_outputs.items():
key = area_type.value if hasattr(area_type, 'value') else str(area_type)
result[key] = area_data
# 映射模式: 用W_s覆盖sensory, 用P投影motor
if self._W_s is not None:
# W_s直接映射: 输入→sensory(跳过随机sensory层)
result['sensory'] = (x @ self._W_s).astype(np.float32)
# 确保有motor输出
if 'motor' not in result:
result['motor'] = output
result['_final'] = output
return result
def chat(self, text: str) -> Optional[Dict]:
"""
文字输入 — 语义编码为75维向量后forward
Args:
text: 输入文字
Returns:
{'text': 回复文字, 'confidence': 置信度, 'areas': 区激活信息}
"""
# 语义编码(vecs75查表,替代hash)
from .semantic_encoder import get_encoder
encoder = get_encoder()
vec = encoder.encode(text)
# QA检索: 编码向量匹配训练数据
# cos>=0.75精确匹配, 0.55~0.75模糊匹配(标注低置信度)
qa_answer, qa_sim = self._qa_retrieve(vec, threshold=0.55)
if qa_answer:
mode = 'qa_brain' if qa_sim >= 0.75 else 'qa_brain_fuzzy'
return {
'text': qa_answer,
'confidence': float(qa_sim),
'areas': ['qa_retrieve'],
'decoded_words': [(qa_answer[:8], qa_sim)],
'mode': mode,
}
result = self.forward(vec)
# 从多个区域信号综合计算置信度
confidences = []
for key in ['sensory', 'association', 'prefrontal', 'motor']:
arr = result.get(key)
if isinstance(arr, np.ndarray) and arr.size > 0:
confidences.append(float(np.max(np.abs(arr))))
confidence = max(confidences) if confidences else 0.0
# 语言解码层: motor 300维 → 自回归生成句子
motor_vec = result.get('motor', result.get('_final', np.zeros(1)))
if self._decoder is not None and motor_vec.size >= 300:
generated = self._decoder.decode(motor_vec[:300], top_k=10)
if generated and len(generated) > 1:
return {
'text': generated,
'confidence': confidence,
'areas': list(result.keys()),
'decoded_words': [(generated[:8], confidence)],
'mode': 'language_decoder',
}
# 回退: 最近邻词解码(未训练时)
if self._W_s is not None:
sensory = result.get('sensory', np.zeros(1))
decode_vec = sensory[:75] if sensory.size >= 75 else motor_vec[:min(75, motor_vec.size)]
elif self._P is not None and motor_vec.size >= 300:
decode_vec = (motor_vec[:300] @ self._P).astype(np.float32)
else:
decode_vec = motor_vec
top_words = encoder.decode_nearest(decode_vec, top_k=5)
if top_words and top_words[0][1] > 0.3:
decoded = ' '.join(w for w, s in top_words[:3] if s > 0.3)
else:
decoded = f'置信度:{confidence:.3f}'
return {
'text': f'[Brain] {decoded}',
'confidence': confidence,
'areas': list(result.keys()),
'decoded_words': top_words[:5],
}
def stats(self) -> Dict:
"""返回统计信息"""
area_info = {}
total_params = 0
for area_type, area in self._net.areas.items():
name = area_type.value
col_count = len(area.columns)
mc_count = sum(len(c.micro_columns) for c in area.columns)
# 用微柱的total_params属性统计
params = 0
for col in area.columns:
for mc in col.micro_columns:
if hasattr(mc, 'total_params'):
params += mc.total_params
total_params += params
area_info[name] = {
'area_name': name,
'columns': col_count,
'micro_columns': mc_count,
'params': params,
}
return {
'areas': len(self._net.areas),
'total_params': total_params,
'area_details': area_info,
'forward_count': self._forward_count,
}
def get_area_weights(self, area_name: str) -> np.ndarray:
"""导出指定区权重 — 深度收集所有numpy属性(含_synaptic内部)"""
area_type = AreaType(area_name)
area = self._net.areas[area_type]
weights_list = []
for col in area.columns:
for mc in col.micro_columns:
# 收集mc自身的所有numpy属性(含下划线)
mc._weight_attrs = []
for attr_name in dir(mc):
val = getattr(mc, attr_name, None)
if isinstance(val, np.ndarray) and val.ndim >= 1 and val.size < 1_000_000:
weights_list.append(val.ravel())
mc._weight_attrs.append(attr_name)
# 深入_synaptic收集
syn = getattr(mc, '_synaptic', None)
if syn is not None:
syn._weight_attrs = []
for attr_name in dir(syn):
val = getattr(syn, attr_name, None)
if isinstance(val, np.ndarray) and val.ndim >= 1 and val.size < 1_000_000:
weights_list.append(val.ravel())
syn._weight_attrs.append(attr_name)
if weights_list:
return np.concatenate(weights_list)
return np.array([], dtype=np.float32)
def set_area_weights(self, area_name: str, weights: np.ndarray):
"""导入指定区权重 — 按记录的属性顺序回填(含_synaptic内部)"""
area_type = AreaType(area_name)
area = self._net.areas[area_type]
offset = 0
for col in area.columns:
for mc in col.micro_columns:
# 回填mc自身属性
for attr_name in getattr(mc, '_weight_attrs', []):
val = getattr(mc, attr_name, None)
if isinstance(val, np.ndarray) and val.ndim >= 1:
n = val.size
if offset + n <= weights.size:
setattr(mc, attr_name, weights[offset:offset+n].reshape(val.shape))
offset += n
# 回填_synaptic属性
syn = getattr(mc, '_synaptic', None)
if syn is not None:
for attr_name in getattr(syn, '_weight_attrs', []):
val = getattr(syn, attr_name, None)
if isinstance(val, np.ndarray) and val.ndim >= 1:
n = val.size
if offset + n <= weights.size:
setattr(syn, attr_name, weights[offset:offset+n].reshape(val.shape))
offset += n
def train_decoder(self, qa_data: list, epochs: int = 10, lr: float = 0.01) -> Dict:
"""训练语言解码层 — 用QA数据teacher forcing训练W_out映射
Args:
qa_data: [{'q': 问题, 'a': 答案}, ...]
epochs: 训练轮数
lr: 学习率
"""
if self._decoder is None:
return {'error': '解码层未初始化'}
from .semantic_encoder import get_encoder
encoder = get_encoder()
total_loss = 0.0
n_trained = 0
for epoch in range(epochs):
epoch_loss = 0.0
for item in qa_data:
q = item.get('q') or item.get('question', '')
a = item.get('a') or item.get('answer', '')
if not q or not a:
continue
# 问题编码 → forward获取motor输出
vec = encoder.encode(q)
if vec is None:
continue
result = self.forward(vec)
motor_vec = result.get('motor', np.zeros(self.config.get('motor_output_dim', 300)))
# 维度适配: 截断或填充到decoder的motor_dim
dec_motor_dim = self._decoder.motor_dim
if motor_vec.size < dec_motor_dim:
motor_vec = np.pad(motor_vec, (0, dec_motor_dim - motor_vec.size))
elif motor_vec.size > dec_motor_dim:
motor_vec = motor_vec[:dec_motor_dim]
# 训练一步: motor向量 + 目标答案
result = self._decoder.train_step(motor_vec, a, lr=lr)
epoch_loss += result['loss'] if isinstance(result, dict) else result
n_trained += 1
total_loss = epoch_loss
# 保存权重
try:
dec_path = os.path.join(os.path.dirname(__file__), '..', '..', 'models', 'decoder_weights.npz')
self._decoder.save(dec_path)
except Exception as e:
print(f"[Brain] 解码层权重保存失败: {e}")
return {'epochs': epochs, 'n_trained': n_trained, 'final_loss': total_loss}
def train(self, texts: list, epochs: int = 1, lr: float = 0.01):
"""在线训练 — 赫布学习(训练时临时解冻)
关键:必须forward向量而非字符串,否则走chat()分支绕过AreaNetwork
"""
import time as _t
from .semantic_encoder import get_encoder
encoder = get_encoder()
self._net.unfreeze()
try:
for epoch in range(epochs):
for i, text in enumerate(texts):
t0 = _t.time()
vec = encoder.encode(text) if isinstance(text, str) else text
if vec is None:
continue
t1 = _t.time()
self.forward(vec)
t2 = _t.time()
self._net.learn(force=True)
t3 = _t.time()
print(f" train[{i}] encode={t1-t0:.1f}s forward={t2-t1:.1f}s learn={t3-t2:.1f}s", flush=True)
finally:
self._net.freeze()
def fedavg(self, area_name: str, incoming: np.ndarray, node_id: str = '') -> Dict:
"""联邦平均聚合"""
local = self.get_area_weights(area_name)
if local.size == 0:
self.set_area_weights(area_name, incoming)
return {'action': 'adopt', 'node': node_id}
# 简单平均
min_len = min(local.size, incoming.size)
avg = (local[:min_len] + incoming[:min_len]) / 2
# 补齐
if local.size > min_len:
avg = np.concatenate([avg, local[min_len:]])
elif incoming.size > min_len:
avg = np.concatenate([avg, incoming[min_len:]])
self.set_area_weights(area_name, avg.astype(np.float32))
return {'action': 'avg', 'local_size': local.size, 'incoming_size': incoming.size}
@property
def areas(self) -> Dict[str, object]:
"""兼容: 返回 {区名str: FunctionalArea}"""
return {at.value: area for at, area in self._net.areas.items()}
def get_stats(self) -> Dict:
"""兼容旧接口"""
return self.stats()
def save(self, path: str):
"""保存模型"""
all_weights = {}
for area_type, area in self._net.areas.items():
name = area_type.value
all_weights[name] = self.get_area_weights(name)
np.savez(path, **all_weights)
def load(self, path: str):
"""加载模型"""
data = np.load(path, allow_pickle=True)
for name in data.files:
try:
self.set_area_weights(name, data[name])
except (ValueError, KeyError):
pass