File size: 8,468 Bytes
f73f9b3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 | import torch
import torch.nn as nn
from .encoder import LLMEncoder
from .mdp_head import MDPHead
from .decision_heads import DecisionHeads
from .domain_moe import DomainMoE
class SystemOneModel(nn.Module):
"""System One 决策链模型:完整组装。
架构:
1. LLM 编码器: 文本 → 语义嵌入(潜空间)
2. MDP Head: 隐式多步 Bellman 前瞻
3. 决策头族: 并行输出 Choice / Transition / Return / Confidence
关键设计:内部隐式多步前瞻,对外紧凑下一步输出。
+-- 2025-09-24: 逐动作架构重构 --+
所有 MLP 接受 [scene_emb ∥ action_vec[a]] 作为特征,对每个动作独立打分,
动作数 n 由运行时 action_vec 的形状决定,
不再依赖固定 num_actions 槽位,不再使用 one-hot 位置编码。
"""
def __init__(self, config):
super().__init__()
self.config = config
self.num_outcomes = config.get("num_outcomes", 2)
self.use_cross_attention = config.get("use_cross_attention", False)
self.encoder = LLMEncoder(config)
self.mdp_head = MDPHead(config)
self.decision_heads = DecisionHeads(config)
# 领域专家 MoE(默认禁用:config.domains 为空时自动关闭)
self.use_domain_experts = config.get("use_domain_experts", False)
self.domain_moe = DomainMoE(config)
def encode_options(self, actions_texts, device=None):
"""编码选项文本列表为动作语义嵌入(支持每样本不同动作数)。
Args:
actions_texts: list of list[str],每个样本的选项文本列表
(每样本动作数可以不同)
device: 目标设备
Returns:
action_embeddings: (batch, batch_max_n, hidden_dim) 每个选项的语义嵌入
num_valid: (batch,) 每个样本的有效动作数
"""
batch_size = len(actions_texts)
num_valid = torch.tensor(
[len(row) for row in actions_texts], dtype=torch.long
)
batch_max_n = num_valid.max().item()
flat_texts = []
for row in actions_texts:
flat_texts.extend(row)
# padding 到 batch_max_n;padding 条目用占位符,编码后会被 mask
pad_count = batch_max_n - len(row)
flat_texts.extend(["无" for _ in range(pad_count)])
flat_emb = self.encoder(flat_texts)
action_embeddings = flat_emb.view(batch_size, batch_max_n, -1)
if device is not None:
action_embeddings = action_embeddings.to(device)
num_valid = num_valid.to(device)
return action_embeddings, num_valid
def _build_action_vec(self, embeddings, action_texts, num_valid_actions):
"""构建 action_vec: (batch, n, hidden_dim)。
语义模式:对各选项文本编码。
槽位模式:用可学习的位置嵌入表。
"""
if action_texts is not None:
action_embeddings, inferred_num_valid = self.encode_options(
action_texts, device=embeddings.device
)
if num_valid_actions is None:
num_valid_actions = inferred_num_valid
return action_embeddings, num_valid_actions
else:
max_n = self.mdp_head.max_actions
n = num_valid_actions.max().item() if num_valid_actions is not None else max_n
indices = (
torch.arange(max_n, device=embeddings.device)
.unsqueeze(0)
.expand(embeddings.shape[0], -1)
)
act_vec = self.mdp_head.action_embed_table(indices)
if n < max_n:
act_vec = act_vec[:, :n, :]
return act_vec, num_valid_actions
def _compute_domain_cond(self, scene_emb):
"""计算领域条件向量(领域 MoE 自动路由)。
无需外部传入领域标签——门控网络从场景语义自动判断激活哪些专家。
Args:
scene_emb: (batch, hidden_dim) 场景语义嵌入
Returns:
cond: (batch, hidden_dim) 领域条件向量;未启用时返回 None
"""
if not self.use_domain_experts or not self.domain_moe.enabled:
return None
return self.domain_moe(scene_emb)
def forward(
self,
texts,
action_texts=None,
return_mdp_details=False,
num_valid_actions=None,
):
"""完整前向传播。
Args:
texts: List[str] 输入文本
action_texts: list of list[str],每个样本的选项文本(可选)
return_mdp_details: 是否返回 MDP 内部细节
num_valid_actions: (batch,) 有效动作数(用于 padding mask)
Returns:
result: dict 包含所有决策输出
"""
embeddings = self.encoder(texts)
action_vec, num_valid = self._build_action_vec(
embeddings, action_texts, num_valid_actions
)
# 领域条件向量由门控网络从场景语义自动路由,无需外部领域标签
cond = self._compute_domain_cond(embeddings)
q_summary, q_history, rewards, attended_emb = self.mdp_head(
embeddings, action_vec, num_valid, cond
)
choices, choice_probs, transition_probs, expected_returns, confidences = (
self.decision_heads(attended_emb, action_vec, num_valid, cond)
)
result = {
"choices": choices,
"choice_probs": choice_probs,
"transition_probs": transition_probs,
"expected_returns": expected_returns,
"q_values": q_summary,
"confidences": confidences.squeeze(-1),
"embeddings": embeddings,
}
if return_mdp_details:
result["q_history"] = q_history
result["rewards"] = rewards
return result
def generate_compact_output(self, texts, action_texts=None, num_valid_actions=None):
"""生成紧凑的下一步输出(符合输出契约)。
Args:
texts: List[str] 输入文本
action_texts: list of list[str],每个样本的选项文本(可选)
num_valid_actions: (batch,) 有效动作数
Returns:
outputs: List[dict] 每个样本的紧凑输出
"""
embeddings = self.encoder(texts)
action_vec, num_valid = self._build_action_vec(
embeddings, action_texts, num_valid_actions
)
# 领域条件向量由门控网络从场景语义自动路由
cond = self._compute_domain_cond(embeddings)
q_summary, q_history, rewards, attended_emb = self.mdp_head(
embeddings, action_vec, num_valid, cond
)
choices, choice_probs, transition_probs, expected_returns, confidences = (
self.decision_heads(attended_emb, action_vec, num_valid, cond)
)
n_actions = action_vec.shape[1]
outputs = []
batch_size = len(texts)
for i in range(batch_size):
transition_dict = {}
for a in range(n_actions):
transition_dict[f"action_{a}"] = (
transition_probs[i, a].detach().cpu().tolist()
)
output = {
"answers": {
"q1": {
"type": "choice",
"choice": f"选项{choices[i].item() + 1}",
"prob_dist": transition_dict,
"expected_return": round(
expected_returns[i, choices[i]].item(), 4
),
"confidence": round(confidences[i].item(), 4),
}
}
}
outputs.append(output)
return outputs
def get_trainable_parameters(self):
"""获取可训练参数(用于优化器)。"""
trainable_params = []
for name, param in self.named_parameters():
if param.requires_grad:
trainable_params.append(param)
return trainable_params
def count_parameters(self):
"""统计参数数量。"""
total = sum(p.numel() for p in self.parameters())
trainable = sum(p.numel() for p in self.parameters() if p.requires_grad)
return total, trainable
|