Radgraph-IT / transformer_block.py
roccoangelella's picture
Add transformers-compatible wrapper (AutoModel/AutoTokenizer via trust_remote_code)
0c48771 verified
Raw
History Blame Contribute Delete
2.88 kB
"""Transformer-block classifier body: self-attention across the span dimension, used by
ner_head.py / relation_head.py in the `medbit_transformer` arm in place of the plain
`FeedForward` scorer (see notes/v2_plan.md item 1).
A standard `nn.TransformerEncoderLayer` already bundles self-attention with its own
position-wise FFN sublayer (residual + norm around both), so this fully replaces
`FeedForward` rather than wrapping it -- no separate downstream MLP survives; callers add
only a final `nn.Linear` to project to label logits, same shape as today.
Spans are the "sequence" here (length = number of candidate/pruned spans, not number of
words); there is no batch dimension and no padding mask, matching every other module's
batch_size=1 simplification (see pruner.py, span_extractor.py). A sinusoidal positional
encoding keyed on each span's start-word index gives attention a notion of span order/distance
-- the same choice already made in v2_plan.md item 1 for the (superseded) word-level
context_layer idea, just applied to span positions instead of word positions.
"""
import math
import torch
from torch import nn
def sinusoidal_position_encoding(positions: torch.Tensor, d_model: int) -> torch.Tensor:
"""positions: (N,) int64 span start indices. Returns (N, d_model)."""
div_term = torch.exp(torch.arange(0, d_model, 2, device=positions.device, dtype=torch.float32) *
(-math.log(10000.0) / d_model))
angles = positions.unsqueeze(1).to(torch.float32) * div_term.unsqueeze(0) # (N, d_model/2)
pe = torch.zeros(positions.size(0), d_model, device=positions.device)
pe[:, 0::2] = torch.sin(angles)
pe[:, 1::2] = torch.cos(angles[:, :pe[:, 1::2].size(1)])
return pe
class TransformerBlock(nn.Module):
def __init__(self, input_dim: int, d_model: int, nhead: int, num_layers: int,
dim_feedforward: int, dropout: float, activation: str = "gelu"):
super().__init__()
self.input_proj = nn.Linear(input_dim, d_model) if input_dim != d_model else nn.Identity()
layer = nn.TransformerEncoderLayer(
d_model=d_model, nhead=nhead, dim_feedforward=dim_feedforward, dropout=dropout,
activation=activation, norm_first=True, batch_first=True)
self.encoder = nn.TransformerEncoder(layer, num_layers=num_layers, enable_nested_tensor=False)
self.d_model = d_model
self.output_dim = d_model
def forward(self, span_embeddings: torch.Tensor, start_positions: torch.Tensor) -> torch.Tensor:
"""span_embeddings: (num_spans, input_dim). start_positions: (num_spans,) int64.
No batch dim on the way in/out -- one fake batch of size 1 for nn.TransformerEncoder."""
x = self.input_proj(span_embeddings) + sinusoidal_position_encoding(start_positions, self.d_model)
return self.encoder(x.unsqueeze(0)).squeeze(0)