trace-deep-ar / modeling_trace.py
jumafernandez's picture
Release in transformers format
97f9ce8 verified
Raw History Blame Contribute Delete
12.8 kB
"""TRACE and TRACE-Dialog in the transformers format.
TRACE is a BERT-style encoder whose inputs are not tokens but the embeddings of the turns of a dialogue, produced by
a base turn encoder (Dialog2Flow by default). It returns a contextual representation ``h_t`` of each turn; in the
autoregressive mode each turn attends only to past turns, so ``h_t`` can be computed during the conversation.
TRACE-Dialog adds learned [CLS] and [SEP] vectors in the input space and returns ``h_CLS``, a representation of the
whole dialogue.
The layers follow ``modeling_bert.py`` with three differences: continuous inputs projected to the hidden size,
speaker embeddings instead of token types, and positions that index turns.
"""
from __future__ import annotations
import numpy as np
import torch
from torch import nn
from transformers import PreTrainedModel
from .configuration_trace import TraceConfig
CLS_ID, SEP_ID = 1, 2
ACT2FN = {"gelu": nn.GELU(), "relu": nn.ReLU(), "tanh": nn.Tanh()}
def eager_attention_forward(module, query, key, value, attention_mask, scaling, dropout=0.0):
attn_weights = torch.matmul(query, key.transpose(2, 3)) * scaling
if attention_mask is not None:
attn_weights = attn_weights + attention_mask
attn_weights = nn.functional.softmax(attn_weights, dim=-1)
attn_weights = nn.functional.dropout(attn_weights, p=dropout, training=module.training)
attn_output = torch.matmul(attn_weights, value)
return attn_output.transpose(1, 2).contiguous(), attn_weights
class TraceSelfAttention(nn.Module):
def __init__(self, config: TraceConfig):
super().__init__()
self.attention_head_size = config.hidden_size // config.num_attention_heads
self.scaling = self.attention_head_size ** -0.5
self.query = nn.Linear(config.hidden_size, config.hidden_size)
self.key = nn.Linear(config.hidden_size, config.hidden_size)
self.value = nn.Linear(config.hidden_size, config.hidden_size)
self.dropout = nn.Dropout(config.attention_probs_dropout_prob)
def forward(self, hidden_states, attention_mask=None):
input_shape = hidden_states.shape[:-1]
shape = (*input_shape, -1, self.attention_head_size)
q = self.query(hidden_states).view(shape).transpose(1, 2)
k = self.key(hidden_states).view(shape).transpose(1, 2)
v = self.value(hidden_states).view(shape).transpose(1, 2)
out, _ = eager_attention_forward(self, q, k, v, attention_mask, self.scaling,
dropout=self.dropout.p if self.training else 0.0)
return out.reshape(*input_shape, -1).contiguous()
class TraceSelfOutput(nn.Module):
def __init__(self, config: TraceConfig):
super().__init__()
self.dense = nn.Linear(config.hidden_size, config.hidden_size)
self.LayerNorm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
def forward(self, hidden_states, input_tensor):
return self.LayerNorm(self.dropout(self.dense(hidden_states)) + input_tensor)
class TraceAttention(nn.Module):
def __init__(self, config: TraceConfig):
super().__init__()
self.self = TraceSelfAttention(config)
self.output = TraceSelfOutput(config)
def forward(self, hidden_states, attention_mask=None):
return self.output(self.self(hidden_states, attention_mask), hidden_states)
class TraceIntermediate(nn.Module):
def __init__(self, config: TraceConfig):
super().__init__()
self.dense = nn.Linear(config.hidden_size, config.intermediate_size)
self.intermediate_act_fn = ACT2FN[config.hidden_act]
def forward(self, hidden_states):
return self.intermediate_act_fn(self.dense(hidden_states))
class TraceOutput(nn.Module):
def __init__(self, config: TraceConfig):
super().__init__()
self.dense = nn.Linear(config.intermediate_size, config.hidden_size)
self.LayerNorm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
def forward(self, hidden_states, input_tensor):
return self.LayerNorm(self.dropout(self.dense(hidden_states)) + input_tensor)
class TraceLayer(nn.Module):
def __init__(self, config: TraceConfig):
super().__init__()
self.attention = TraceAttention(config)
self.intermediate = TraceIntermediate(config)
self.output = TraceOutput(config)
def forward(self, hidden_states, attention_mask=None):
attention_output = self.attention(hidden_states, attention_mask)
return self.output(self.intermediate(attention_output), attention_output)
class TraceEncoder(nn.Module):
def __init__(self, config: TraceConfig):
super().__init__()
self.layer = nn.ModuleList([TraceLayer(config) for _ in range(config.num_hidden_layers)])
def forward(self, hidden_states, attention_mask=None):
for layer in self.layer:
hidden_states = layer(hidden_states, attention_mask)
return hidden_states
class TraceEmbeddings(nn.Module):
"""Turn embeddings projected to the hidden size, plus speaker and position embeddings."""
def __init__(self, config: TraceConfig):
super().__init__()
self.input_proj = (nn.Linear(config.input_dim, config.hidden_size)
if config.input_dim != config.hidden_size else nn.Identity())
self.position_embeddings = nn.Embedding(config.max_position_embeddings, config.hidden_size)
self.speaker_embeddings = (nn.Embedding(config.num_speakers, config.hidden_size)
if config.use_speaker_embeddings else None)
self.num_speakers = config.num_speakers
self.LayerNorm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
def forward(self, inputs_embeds, speaker_ids=None):
embeddings = self.input_proj(inputs_embeds)
if self.speaker_embeddings is not None and speaker_ids is not None:
embeddings = embeddings + self.speaker_embeddings(speaker_ids.clamp(min=0, max=self.num_speakers - 1))
positions = torch.arange(inputs_embeds.shape[1], device=inputs_embeds.device).unsqueeze(0)
return self.dropout(self.LayerNorm(embeddings + self.position_embeddings(positions)))
class TraceBody(nn.Module):
"""Embeddings and encoder (the counterpart of ``BertModel`` without pooler)."""
def __init__(self, config: TraceConfig):
super().__init__()
self.config = config
self.embeddings = TraceEmbeddings(config)
self.encoder = TraceEncoder(config)
def extended_mask(self, attention_mask, seq_len, dtype):
allowed = attention_mask[:, None, None, :].bool()
if self.config.is_causal:
causal = torch.tril(torch.ones(seq_len, seq_len, dtype=torch.bool, device=attention_mask.device))
allowed = allowed & causal[None, None]
mask = torch.zeros(allowed.shape, dtype=dtype, device=attention_mask.device)
return mask.masked_fill(~allowed, torch.finfo(dtype).min)
def forward(self, inputs_embeds, attention_mask, speaker_ids=None):
hidden = self.embeddings(inputs_embeds, speaker_ids)
return self.encoder(hidden, self.extended_mask(attention_mask, inputs_embeds.shape[1], hidden.dtype))
class TracePreTrainedModel(PreTrainedModel):
config_class = TraceConfig
base_model_prefix = "bert"
def _init_weights(self, module):
std = self.config.initializer_range
if isinstance(module, nn.Linear):
module.weight.data.normal_(mean=0.0, std=std)
if module.bias is not None:
module.bias.data.zero_()
elif isinstance(module, nn.Embedding):
module.weight.data.normal_(mean=0.0, std=std)
elif isinstance(module, nn.LayerNorm):
module.bias.data.zero_()
module.weight.data.fill_(1.0)
# ------------------------------------------------------------------ helpers shared by both models
def speaker_ids(self, speakers: list[str]) -> list[int]:
unknown = self.config.num_speakers - 1
return [min(self.config.speaker_map.get(str(s).strip().lower(), unknown), unknown) for s in speakers]
def base_embeddings(self, texts: list[str], base_encoder=None) -> np.ndarray:
"""Turn embeddings with the base encoder the model was trained on (loaded on first use)."""
if base_encoder is None:
if getattr(self, "_base", None) is None:
from sentence_transformers import SentenceTransformer
self._base = SentenceTransformer(self.config.base_encoder, device=str(self.device))
base_encoder = self._base
return np.asarray(base_encoder.encode(texts, batch_size=64, show_progress_bar=False), dtype=np.float32)
class TraceModel(TracePreTrainedModel):
"""TRACE: contextual representation ``h_t`` of every turn of a dialogue."""
def __init__(self, config: TraceConfig):
super().__init__(config)
self.bert = TraceBody(config)
self.output_proj = (nn.Linear(config.hidden_size, config.output_dim)
if config.hidden_size != config.output_dim else nn.Identity())
self.post_init()
def forward(self, inputs_embeds, attention_mask, speaker_ids=None):
if inputs_embeds.shape[1] > self.config.max_turns:
raise ValueError(f"{inputs_embeds.shape[1]} turns exceed max_turns ({self.config.max_turns})")
return self.output_proj(self.bert(inputs_embeds, attention_mask, speaker_ids))
@torch.no_grad()
def encode_dialogue(self, turns: list[str], speakers: list[str], embeddings: np.ndarray | None = None,
base_encoder=None) -> np.ndarray:
"""``h_t`` of every turn, shape ``(n, output_dim)``. Dialogues longer than ``max_turns`` are split into
consecutive windows. Pass ``embeddings`` to reuse precomputed base turn embeddings."""
self.eval()
e = embeddings if embeddings is not None else self.base_embeddings(turns, base_encoder)
spk = self.speaker_ids(speakers)
out = []
for start in range(0, len(turns), self.config.max_turns):
end = start + self.config.max_turns
x = torch.as_tensor(e[start:end], dtype=self.dtype, device=self.device)[None]
s = torch.as_tensor(spk[start:end], device=self.device)[None]
att = torch.ones(x.shape[:2], dtype=torch.long, device=self.device)
out.append(self(x, att, s if self.config.use_speaker_embeddings else None)[0].float().cpu().numpy())
return np.concatenate(out)
class TraceDialogModel(TraceModel):
"""TRACE-Dialog: representation ``h_CLS`` of a whole dialogue, from [CLS] turns [SEP]."""
def __init__(self, config: TraceConfig):
super().__init__(config)
self.special_embeddings = nn.Embedding(2, config.input_dim)
self.post_init()
def forward(self, inputs_embeds, attention_mask, speaker_ids=None, special_ids=None):
if special_ids is not None:
x = inputs_embeds.clone()
x[special_ids == CLS_ID] = self.special_embeddings.weight[0].to(x.dtype)
x[special_ids == SEP_ID] = self.special_embeddings.weight[1].to(x.dtype)
inputs_embeds = x
return super().forward(inputs_embeds, attention_mask, speaker_ids)
@torch.no_grad()
def encode_dialogue(self, turns: list[str], speakers: list[str], embeddings: np.ndarray | None = None,
base_encoder=None) -> np.ndarray:
"""``h_CLS`` of the dialogue, shape ``(output_dim,)``. Only the first ``max_turns - 2`` turns are used."""
self.eval()
e = embeddings if embeddings is not None else self.base_embeddings(turns, base_encoder)
n = min(len(turns), self.config.max_turns - 2)
x = torch.zeros(1, n + 2, self.config.input_dim, dtype=self.dtype, device=self.device)
x[0, 1:n + 1] = torch.as_tensor(e[:n], dtype=self.dtype, device=self.device)
spk = torch.full((1, n + 2), self.config.num_speakers - 1, dtype=torch.long, device=self.device)
spk[0, 1:n + 1] = torch.as_tensor(self.speaker_ids(speakers[:n]), device=self.device)
special = torch.zeros(1, n + 2, dtype=torch.long, device=self.device)
special[0, 0], special[0, n + 1] = CLS_ID, SEP_ID
att = torch.ones(1, n + 2, dtype=torch.long, device=self.device)
h = self(x, att, spk if self.config.use_speaker_embeddings else None, special)
return h[0, 0].float().cpu().numpy()