RAIC / modeling_ftt.py
thinkhong's picture
Upload FTT UKB CVD classification model (20240603_1_UKB_CVD_cls_hp_search)
86f5ee3 verified
Raw History Blame Contribute Delete
16 kB
"""
FTT (Feature Tokenizer Transformer) for tabular UK Biobank data.
A DistilBERT-style transformer adapted to tabular inputs: each row is
treated as a "sentence" whose "tokens" are its features. Continuous
features are embedded via a per-feature learned weight vector scaled by
the (min-max normalized) value; categorical features are embedded via a
per-value lookup table. A separate embedding table encodes which feature
slot ("variable") each token corresponds to, functioning like a position
embedding.
This file is loaded automatically via `trust_remote_code=True` through the
`auto_map` entries in config.json.
"""
from typing import Optional, Tuple, Union
from transformers.models.distilbert.modeling_distilbert import (
Embeddings,
DistilBertModel,
Transformer,
DistilBertForMaskedLM,
DistilBertForSequenceClassification,
)
from transformers.modeling_attn_mask_utils import _prepare_4d_attention_mask_for_sdpa
from transformers import PretrainedConfig
from transformers.modeling_outputs import BaseModelOutput, MaskedLMOutput, SequenceClassifierOutput
from transformers.utils import logging
from transformers.activations import get_activation
import torch
from torch import nn
from torch.nn import BCEWithLogitsLoss, CrossEntropyLoss, MSELoss
import torch.nn.functional as F
logger = logging.get_logger(__name__)
class FTTConfig(PretrainedConfig):
"""Config for the FTT tabular transformer (DistilBERT-style hyperparameters)."""
model_type = "ftt"
def __init__(
self,
vocab_size: int = 512,
var_weights_size: int = 512,
var_bias_size: int = 512,
max_position_embeddings: int = 256,
sinusoidal_pos_embds: bool = False,
n_layers: int = 8,
n_heads: int = 16,
dim: int = 128,
hidden_dim: int = 1024,
dropout: float = 0.1,
attention_dropout: float = 0.1,
activation: str = "gelu",
initializer_range: float = 0.02,
qa_dropout: float = 0.1,
seq_classif_dropout: float = 0.2,
tie_weights_: bool = True,
pad_token_id: int = 3,
**kwargs,
):
self.vocab_size = vocab_size
self.var_weights_size = var_weights_size
self.var_bias_size = var_bias_size
self.max_position_embeddings = max_position_embeddings
self.sinusoidal_pos_embds = sinusoidal_pos_embds
self.n_layers = n_layers
self.num_hidden_layers = n_layers
self.n_heads = n_heads
self.dim = dim
self.hidden_dim = hidden_dim
self.dropout = dropout
self.attention_dropout = attention_dropout
self.activation = activation
self.initializer_range = initializer_range
self.qa_dropout = qa_dropout
self.seq_classif_dropout = seq_classif_dropout
self.tie_weights_ = tie_weights_
super().__init__(pad_token_id=pad_token_id, **kwargs)
class TableEmbeddings(Embeddings):
def __init__(self, config: PretrainedConfig):
super().__init__(config)
self.variable_weights_embeddings = nn.Embedding(config.var_weights_size, config.dim, padding_idx=config.pad_token_id)
self.variable_bias_embeddings = nn.Embedding(config.max_position_embeddings, config.dim, padding_idx=config.pad_token_id)
if config.sinusoidal_pos_embds:
raise NotImplementedError("We don't use sinusoidal position embeddings in gene embedding")
self.LayerNorm = nn.LayerNorm(config.dim, eps=1e-12)
self.dropout = nn.Dropout(config.dropout)
self.config = config
def forward(
self,
num_input_val: Optional[torch.Tensor] = None,
num_input_ids: Optional[torch.Tensor] = None,
cat_input_ids: Optional[torch.Tensor] = None,
num_variable_ids: Optional[torch.Tensor] = None,
cat_variable_ids: Optional[torch.Tensor] = None,
input_embeds: Optional[torch.Tensor] = None,
) -> torch.Tensor:
if num_input_ids is None and cat_input_ids is None:
raise ValueError("any num_input_ids or cat_input_ids is required")
if num_input_ids is not None:
num_embed = self.variable_weights_embeddings(num_input_ids)
num_embed = torch.einsum("bl,bld->bld", num_input_val, num_embed)
if cat_input_ids is not None:
cat_embed = self.variable_weights_embeddings(cat_input_ids)
if num_input_val is None and cat_input_ids is not None:
weights_embeddings = cat_embed
bias_embeddings = self.variable_bias_embeddings(cat_variable_ids)
elif num_input_val is not None and cat_input_ids is None:
weights_embeddings = num_embed
bias_embeddings = self.variable_bias_embeddings(num_variable_ids)
else:
weights_embeddings = torch.cat([num_embed, cat_embed], dim=1)
bias_embeddings = self.variable_bias_embeddings(torch.cat([num_variable_ids, cat_variable_ids], dim=1))
embeddings = weights_embeddings + bias_embeddings
embeddings = self.LayerNorm(embeddings)
embeddings = self.dropout(embeddings)
return embeddings
class FTTModel(DistilBertModel):
config_class = FTTConfig
def __init__(self, config: PretrainedConfig):
super().__init__(config)
self.embeddings = TableEmbeddings(config)
self.transformer = Transformer(config)
self._use_flash_attention_2 = config._attn_implementation == "flash_attention_2"
self.post_init()
def forward(
self,
num_input_val: Optional[torch.Tensor] = None,
num_input_ids: Optional[torch.Tensor] = None,
cat_input_ids: Optional[torch.Tensor] = None,
num_variable_ids: Optional[torch.Tensor] = None,
cat_variable_ids: Optional[torch.Tensor] = None,
attention_mask: Optional[torch.Tensor] = None,
head_mask: Optional[torch.Tensor] = None,
inputs_embeds: Optional[torch.Tensor] = None,
output_attentions: Optional[bool] = None,
output_hidden_states: Optional[bool] = None,
return_dict: Optional[bool] = None,
) -> Union[BaseModelOutput, Tuple[torch.Tensor, ...]]:
output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
output_hidden_states = (
output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
)
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
if inputs_embeds is not None:
raise ValueError("inputs_embeds is not supported")
elif num_input_val is None and cat_input_ids is None:
raise ValueError("any num_input_ids or cat_input_ids is required")
if num_input_val is not None and cat_input_ids is not None:
input_shape = torch.tensor(num_input_val.size())
input_shape[1] = input_shape[1] + cat_input_ids.size(1)
embeddings = self.embeddings(
num_input_val,
num_input_ids,
cat_input_ids,
num_variable_ids,
cat_variable_ids,
)
device = embeddings.device
head_mask_is_none = head_mask is None
head_mask = self.get_head_mask(head_mask, self.config.num_hidden_layers)
if self._use_flash_attention_2:
attention_mask = attention_mask if (attention_mask is not None and 0 in attention_mask) else None
else:
if attention_mask is None:
attention_mask = torch.ones(input_shape, device=device)
if self._use_sdpa and head_mask_is_none and not output_attentions:
attention_mask = _prepare_4d_attention_mask_for_sdpa(
attention_mask, embeddings.dtype, tgt_len=input_shape[1]
)
return self.transformer(
x=embeddings,
attn_mask=attention_mask,
head_mask=head_mask,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict,
)
class IgnoreMaskingMSELoss(nn.Module):
def __init__(self, ignore_index=-100):
super(IgnoreMaskingMSELoss, self).__init__()
self.ignore_index = ignore_index
def forward(self, prediction, target):
mask = target != self.ignore_index
filtered_pred = prediction[mask]
filtered_target = target[mask]
return F.mse_loss(filtered_pred, filtered_target)
class FTTForMaskedLM(DistilBertForMaskedLM):
config_class = FTTConfig
_tied_weights_keys = ["vocab_projector.weight"]
def __init__(self, config: PretrainedConfig):
super().__init__(config)
self.activation = get_activation(config.activation)
self.distilbert = FTTModel(config)
self.vocab_transform = nn.Linear(config.dim, config.dim)
self.vocab_layer_norm = nn.LayerNorm(config.dim, eps=1e-12)
self.vocab_projector = nn.Linear(config.dim, config.vocab_size)
self.num_projector = nn.Linear(config.dim, 1)
self.post_init()
self.mlm_loss_fct = nn.CrossEntropyLoss()
self.mlm_loss_mse = IgnoreMaskingMSELoss()
def forward(
self,
num_input_val: Optional[torch.Tensor] = None,
num_input_ids: Optional[torch.Tensor] = None,
cat_input_ids: Optional[torch.Tensor] = None,
num_variable_ids: Optional[torch.Tensor] = None,
cat_variable_ids: Optional[torch.Tensor] = None,
attention_mask: Optional[torch.Tensor] = None,
head_mask: Optional[torch.Tensor] = None,
inputs_embeds: Optional[torch.Tensor] = None,
labels: Optional[torch.LongTensor] = None,
output_attentions: Optional[bool] = None,
output_hidden_states: Optional[bool] = None,
return_dict: Optional[bool] = None,
) -> Union[MaskedLMOutput, Tuple[torch.Tensor, ...]]:
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
dlbrt_output = self.distilbert(
num_input_val=num_input_val,
num_input_ids=num_input_ids,
cat_input_ids=cat_input_ids,
num_variable_ids=num_variable_ids,
cat_variable_ids=cat_variable_ids,
attention_mask=attention_mask,
head_mask=head_mask,
inputs_embeds=inputs_embeds,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict,
)
hidden_states = dlbrt_output[0]
prediction_logits = self.vocab_transform(hidden_states)
prediction_logits = self.activation(prediction_logits)
prediction_logits = self.vocab_layer_norm(prediction_logits)
continuous_prediction_logits = self.num_projector(prediction_logits[:, : num_variable_ids.size(1), :])
continuous_prediction_logits = continuous_prediction_logits.squeeze(-1)
categorical_prediction_logits = self.vocab_projector(prediction_logits[:, num_variable_ids.size(1) :, :])
mlm_loss = None
if labels is not None:
continuous_labels = labels[:, : num_variable_ids.size(1)]
continuous_mlm_loss = self.mlm_loss_mse(continuous_prediction_logits, continuous_labels)
categorical_labels = labels[:, num_variable_ids.size(1) :].type(torch.LongTensor).to(device=self.distilbert.device)
categorical_mlm_loss = self.mlm_loss_fct(
categorical_prediction_logits.view(-1, categorical_prediction_logits.size(-1)),
categorical_labels.reshape(-1),
)
mlm_loss = continuous_mlm_loss + categorical_mlm_loss
else:
return {
"continuous_prediction_logits": continuous_prediction_logits,
"categorical_prediction_logits": categorical_prediction_logits,
}
return MaskedLMOutput(
loss=mlm_loss,
logits=prediction_logits,
hidden_states=dlbrt_output.hidden_states,
attentions=dlbrt_output.attentions,
)
class FTTForSequenceClassification(DistilBertForSequenceClassification):
config_class = FTTConfig
def __init__(self, config: PretrainedConfig):
super().__init__(config)
self.num_labels = config.num_labels
self.config = config
self.distilbert = FTTModel(config)
self.pre_classifier = nn.Linear(config.dim, config.dim)
self.classifier = nn.Linear(config.dim, config.num_labels)
self.dropout = nn.Dropout(config.seq_classif_dropout)
self.sigmoid = nn.Sigmoid()
self.post_init()
def forward(
self,
num_input_val: Optional[torch.Tensor] = None,
num_input_ids: Optional[torch.Tensor] = None,
cat_input_ids: Optional[torch.Tensor] = None,
num_variable_ids: Optional[torch.Tensor] = None,
cat_variable_ids: Optional[torch.Tensor] = None,
attention_mask: Optional[torch.Tensor] = None,
head_mask: Optional[torch.Tensor] = None,
inputs_embeds: Optional[torch.Tensor] = None,
labels: Optional[torch.LongTensor] = None,
output_attentions: Optional[bool] = None,
output_hidden_states: Optional[bool] = None,
return_dict: Optional[bool] = None,
) -> Union[SequenceClassifierOutput, Tuple[torch.Tensor, ...]]:
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
distilbert_output = self.distilbert(
num_input_val=num_input_val,
num_input_ids=num_input_ids,
cat_input_ids=cat_input_ids,
num_variable_ids=num_variable_ids,
cat_variable_ids=cat_variable_ids,
attention_mask=attention_mask,
head_mask=head_mask,
inputs_embeds=inputs_embeds,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict,
)
hidden_state = distilbert_output[0]
pooled_output = hidden_state[:, 0]
pooled_output = self.pre_classifier(pooled_output)
pooled_output = nn.ReLU()(pooled_output)
pooled_output = self.dropout(pooled_output)
logits = self.classifier(pooled_output)
loss = None
if labels is not None:
if self.config.problem_type is None:
if self.num_labels == 1:
self.config.problem_type = "regression"
elif self.num_labels > 1 and (labels.dtype == torch.long or labels.dtype == torch.int):
self.config.problem_type = "single_label_classification"
else:
self.config.problem_type = "multi_label_classification"
if self.config.problem_type == "regression":
labels = labels.float()
loss_fct = MSELoss()
if self.num_labels == 1:
loss = loss_fct(logits.squeeze(), labels.squeeze())
else:
loss = loss_fct(logits, labels)
elif self.config.problem_type == "single_label_classification":
loss_fct = CrossEntropyLoss()
loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1))
elif self.config.problem_type == "multi_label_classification":
labels = labels.float()
loss_fct = BCEWithLogitsLoss()
loss = loss_fct(logits.squeeze(), labels)
if not return_dict:
output = (logits,) + distilbert_output[1:]
return ((loss,) + output) if loss is not None else output
return SequenceClassifierOutput(
loss=loss,
logits=logits,
hidden_states=distilbert_output.hidden_states,
attentions=distilbert_output.attentions,
)