banyan-sapling / runtime /interaction_head.py
anilperi's picture
Publish Banyan Sapling research preview
a82e638 verified
Raw History Blame Contribute Delete
4.8 kB
"""Segment-aware interactions between policy, evidence, and claim vectors."""
import torch
from torch import Tensor, nn
class TriVectorInteractionHead(nn.Module):
"""Classify policy/evidence/claim vectors using seven ordered feature blocks."""
def __init__(self, hidden_dim: int, num_classes: int = 4, dropout: float = .1):
super().__init__()
for name, value in (("hidden_dim", hidden_dim), ("num_classes", num_classes)):
if isinstance(value, bool) or not isinstance(value, int):
raise TypeError(f"{name} must be an integer")
if value <= 0:
raise ValueError(f"{name} must be positive")
self.hidden_dim = hidden_dim
self.classifier = nn.Sequential(
nn.Dropout(dropout),
nn.Linear(7 * hidden_dim, 2 * hidden_dim),
nn.GELU(),
nn.Dropout(dropout),
nn.Linear(2 * hidden_dim, num_classes),
)
def construct_features(self, h_P: Tensor, h_E: Tensor, h_C: Tensor) -> Tensor:
"""Return [P, E, C, |P-E|, P*E, |E-C|, E*C] with shape [B, 7H]."""
for vector in (h_P, h_E, h_C):
if not isinstance(vector, Tensor) or not vector.is_floating_point():
raise TypeError("segment vectors must be floating-point tensors")
if vector.ndim != 2 or vector.shape[-1] != self.hidden_dim:
raise ValueError("segment vectors must have shape [B, hidden_dim]")
if vector.shape != h_P.shape or vector.dtype != h_P.dtype or vector.device != h_P.device:
raise ValueError("segment vectors must share shape, dtype, and device")
return torch.cat((h_P, h_E, h_C, (h_P - h_E).abs(), h_P * h_E,
(h_E - h_C).abs(), h_E * h_C), dim=-1)
def forward(self, h_P: Tensor, h_E: Tensor, h_C: Tensor) -> Tensor:
return self.classifier(self.construct_features(h_P, h_E, h_C))
def pool_segments(self, hidden_states: Tensor, segments: Tensor,
attention_mask: Tensor) -> tuple[Tensor, Tensor, Tensor]:
"""Mean-pool [B,L,H] states into policy (1), evidence (2), and claim (3).
Segment 0 denotes delimiters/padding. Only tokens with a binary attention
mask of 1 contribute. Every row must contain an unmasked token in each
segment; empty segments raise ValueError rather than producing NaNs.
Float16/bfloat16 sums and division use float32 to avoid overflow and
count rounding; pooled vectors retain the input dtype and device.
"""
if not isinstance(hidden_states, Tensor) or not hidden_states.is_floating_point():
raise TypeError("hidden_states must be a floating-point tensor")
if hidden_states.ndim != 3 or hidden_states.shape[-1] != self.hidden_dim:
raise ValueError("hidden_states must have shape [B, L, hidden_dim]")
for name, tensor in (("segments", segments), ("attention_mask", attention_mask)):
if not isinstance(tensor, Tensor):
raise TypeError(f"{name} must be a tensor")
if tensor.shape != hidden_states.shape[:2]:
raise ValueError(f"{name} must have shape [B, L]")
if tensor.device != hidden_states.device:
raise ValueError(f"{name} must share the hidden_states device")
if segments.dtype not in (torch.uint8, torch.int8, torch.int16, torch.int32, torch.int64):
raise TypeError("segments must contain integer IDs")
if ((segments < 0) | (segments > 3)).any():
raise ValueError("segment IDs must be 0, 1, 2, or 3")
if not ((attention_mask == 0) | (attention_mask == 1)).all():
raise ValueError("attention_mask must be binary")
accumulation_dtype = (torch.float32 if hidden_states.dtype in (torch.float16, torch.bfloat16)
else hidden_states.dtype)
pooled = []
for segment_id in (1, 2, 3):
selected = (segments == segment_id) & attention_mask.bool()
counts = selected.sum(dim=1, keepdim=True)
if (counts == 0).any():
raise ValueError(f"empty segment {segment_id} in at least one batch row")
masked = hidden_states.masked_fill(~selected.unsqueeze(-1), 0)
mean = masked.sum(dim=1, dtype=accumulation_dtype) / counts.to(accumulation_dtype)
pooled.append(mean.to(hidden_states.dtype))
return tuple(pooled)
def forward_from_hidden(self, hidden_states: Tensor, segments: Tensor,
attention_mask: Tensor) -> Tensor:
"""Pool segment states and return [B, num_classes] classification logits."""
return self(*self.pool_segments(hidden_states, segments, attention_mask))