| """Concept Fusion Layer module for CAT V3.""" |
|
|
| from __future__ import annotations |
|
|
| from typing import Any, Dict, List |
| import torch |
| import torch.nn as nn |
|
|
|
|
| class ConceptFusionLayer(nn.Module): |
| """Fuses path predictions and embeddings from multiple active GAT experts.""" |
|
|
| def __init__(self, num_concepts: int, pad_id: int, eos_id: int, top_m: int = 8) -> None: |
| super().__init__() |
| self.num_concepts = num_concepts |
| self.pad_id = pad_id |
| self.eos_id = eos_id |
| self.top_m = top_m |
|
|
| def forward( |
| self, |
| expert_reports: Dict[str, Dict[str, torch.Tensor]], |
| router_probs: torch.Tensor, |
| router_mask: torch.Tensor, |
| global_embeddings: torch.Tensor, |
| domain_names: List[str], |
| ) -> Dict[str, torch.Tensor]: |
| """Fuses expert reports into a unified tensor representation. |
| |
| Args: |
| expert_reports: Dict mapping domain -> GATExpert output dict |
| router_probs: [batch_size, num_experts] tensor of router probabilities |
| router_mask: [batch_size, num_experts] boolean mask of active experts |
| global_embeddings: [num_concepts, concept_dim] concept embedding table |
| domain_names: List of domain names mapping to indices in router outputs |
| |
| Returns: |
| Dict containing: |
| fused_concept_ids: [batch_size, top_m] |
| fused_embeddings: [batch_size, top_m, concept_dim] |
| fused_scores: [batch_size, num_concepts] |
| top_scores: [batch_size, top_m] |
| """ |
| batch_size = router_probs.size(0) |
| device = router_probs.device |
| path_len = next(iter(expert_reports.values()))["predicted_path"].size(1) |
|
|
| |
| fused_scores = torch.zeros(batch_size, self.num_concepts, device=device) |
|
|
| for idx, domain in enumerate(domain_names): |
| report = expert_reports[domain] |
| pred_path = report["predicted_path"] |
| path_scores = report["path_scores"] |
| path_probs = torch.exp(path_scores) |
|
|
| |
| |
| active_weight = router_probs[:, idx] * router_mask[:, idx].float() |
|
|
| |
| for step in range(path_len): |
| step_concepts = pred_path[:, step] |
| step_probs = path_probs[:, step] |
|
|
| |
| weighted_probs = step_probs * active_weight |
| fused_scores.scatter_add_(1, step_concepts.unsqueeze(1), weighted_probs.unsqueeze(1)) |
|
|
| |
| fused_scores[:, self.pad_id] = 0.0 |
| fused_scores[:, self.eos_id] = 0.0 |
|
|
| |
| scores, fused_concept_ids = torch.topk(fused_scores, k=self.top_m, dim=-1) |
|
|
| |
| fused_embeddings = global_embeddings[fused_concept_ids] |
|
|
| return { |
| "fused_concept_ids": fused_concept_ids, |
| "fused_embeddings": fused_embeddings, |
| "fused_scores": fused_scores, |
| "top_scores": scores |
| } |
|
|
| def get_symbolic_report( |
| self, |
| vocab: Any, |
| expert_reports: Dict[str, Dict[str, torch.Tensor]], |
| router_mask: torch.Tensor, |
| domain_names: List[str], |
| ) -> List[Dict[str, Any]]: |
| """Converts raw tensor results into human-readable concept graphs and paths. |
| |
| Args: |
| vocab: ConceptVocabulary instance |
| expert_reports: Dict of expert outputs |
| router_mask: [batch_size, num_experts] |
| domain_names: List of domains |
| |
| Returns: |
| List of dicts, one per batch item: |
| { |
| "concepts": List[str], |
| "reasoning_paths": List[List[str]], |
| "confidence": List[float] |
| } |
| """ |
| batch_size = router_mask.size(0) |
| symbolic_reports = [] |
|
|
| for b in range(batch_size): |
| concepts_set = set() |
| reasoning_paths = [] |
| confidences = [] |
|
|
| for idx, domain in enumerate(domain_names): |
| if router_mask[b, idx].item(): |
| report = expert_reports[domain] |
| path_ids = report["predicted_path"][b].tolist() |
| path = vocab.decode_path(path_ids) |
| if path: |
| reasoning_paths.append(path) |
| for c in path: |
| concepts_set.add(c) |
| |
| |
| path_scores = report["path_scores"][b].tolist() |
| path_probs = [torch.exp(torch.tensor(s)).item() for s in path_scores[:len(path)]] |
| mean_prob = sum(path_probs) / max(len(path_probs), 1) |
| confidences.append(mean_prob) |
|
|
| symbolic_reports.append({ |
| "concepts": sorted(list(concepts_set)), |
| "reasoning_paths": reasoning_paths, |
| "confidence": confidences |
| }) |
|
|
| return symbolic_reports |
|
|