| """Anchor-linking interfaces kept separate from graph-program compilation.""" |
|
|
| from __future__ import annotations |
|
|
| from dataclasses import dataclass |
|
|
| import torch |
| from torch import nn |
|
|
| from strata.modeling.compose.ast import AnchorRef |
| from strata.modeling.compose.types import SemanticType |
| from strata.modeling.participant_late_interaction import QueryFacetCrossAttentionReader |
|
|
|
|
| @dataclass(frozen=True, slots=True) |
| class AnchorLinkOutput: |
| scores: torch.Tensor |
| selected_index: torch.Tensor |
| anchor_type: SemanticType |
|
|
|
|
| class StructuredAnchorLinker(nn.Module): |
| """Copy an externally verified episode-local anchor without classifying IDs.""" |
|
|
| def forward(self, local_indices: torch.Tensor, *, anchor_type: SemanticType) -> AnchorLinkOutput: |
| if local_indices.ndim != 1: |
| raise ValueError("local_indices must have shape [batch]") |
| candidates = int(local_indices.max().item()) + 1 |
| scores = torch.full( |
| (local_indices.shape[0], candidates), |
| -torch.inf, |
| device=local_indices.device, |
| ) |
| scores.scatter_(1, local_indices.unsqueeze(1), 0.0) |
| return AnchorLinkOutput(scores, local_indices, anchor_type) |
|
|
|
|
| class ParticipantFacetAnchorLinker(nn.Module): |
| """Adapt the frozen four-facet participant primitive to entity anchors.""" |
|
|
| def __init__(self, reader: QueryFacetCrossAttentionReader) -> None: |
| super().__init__() |
| self.reader = reader |
|
|
| def forward( |
| self, |
| query_tokens: torch.Tensor, |
| query_mask: torch.Tensor, |
| query_vector: torch.Tensor, |
| participant_facets: torch.Tensor, |
| ) -> AnchorLinkOutput: |
| scores = self.reader(query_tokens, query_mask, query_vector, participant_facets) |
| return AnchorLinkOutput(scores, scores.argmax(dim=-1), SemanticType.ENTITY) |
|
|
|
|
| class TypedCandidateAnchorLinker(nn.Module): |
| """Set-equivariant event/claim linker; candidate IDs are never embedded.""" |
|
|
| def __init__(self, hidden_dim: int, anchor_type: SemanticType) -> None: |
| super().__init__() |
| self.anchor_type = anchor_type |
| self.query = nn.Linear(hidden_dim, hidden_dim, bias=False) |
| self.candidate = nn.Linear(hidden_dim, hidden_dim, bias=False) |
|
|
| def forward( |
| self, |
| query: torch.Tensor, |
| candidates: torch.Tensor, |
| candidate_mask: torch.Tensor | None = None, |
| ) -> AnchorLinkOutput: |
| scores = torch.einsum("bd,bnd->bn", self.query(query), self.candidate(candidates)) |
| scores = scores / query.shape[-1] ** 0.5 |
| if candidate_mask is not None: |
| scores = scores.masked_fill(~candidate_mask, -torch.inf) |
| return AnchorLinkOutput(scores, scores.argmax(dim=-1), self.anchor_type) |
|
|
|
|
| def copied_anchor(output: AnchorLinkOutput, batch_index: int = 0) -> AnchorRef: |
| return AnchorRef(int(output.selected_index[batch_index]), output.anchor_type) |
|
|
|
|
| __all__ = [ |
| "AnchorLinkOutput", |
| "ParticipantFacetAnchorLinker", |
| "StructuredAnchorLinker", |
| "TypedCandidateAnchorLinker", |
| "copied_anchor", |
| ] |
|
|