"""Transformer-based model for sign language recognition""" import torch import torch.nn as nn import torch.nn.functional as F from typing import Optional import math class PositionalEncoding(nn.Module): """Positional encoding for transformer""" def __init__(self, d_model: int, max_len: int = 5000, dropout: float = 0.1): super().__init__() self.dropout = nn.Dropout(p=dropout) # Create positional encoding matrix pe = torch.zeros(max_len, d_model) position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] = torch.sin(position * div_term) pe[:, 1::2] = torch.cos(position * div_term) pe = pe.unsqueeze(0) self.register_buffer('pe', pe) def forward(self, x: torch.Tensor) -> torch.Tensor: """ Args: x: Tensor of shape (batch_size, seq_len, d_model) """ x = x + self.pe[:, :x.size(1), :] return self.dropout(x) class MediaPipeTransformer(nn.Module): """Transformer model for MediaPipe landmarks""" def __init__(self, input_dim: int = 258, # 33*4 + 21*3 + 21*3 (pose + hands, no face) d_model: int = 256, nhead: int = 8, num_encoder_layers: int = 4, dim_feedforward: int = 1024, dropout: float = 0.3, num_classes: int = 100, max_seq_length: int = 64): """ Args: input_dim: Dimension of input landmarks d_model: Dimension of model embeddings nhead: Number of attention heads num_encoder_layers: Number of transformer encoder layers dim_feedforward: Dimension of feedforward network dropout: Dropout rate num_classes: Number of output classes max_seq_length: Maximum sequence length """ super().__init__() self.input_dim = input_dim self.d_model = d_model self.num_classes = num_classes # Input projection self.input_projection = nn.Sequential( nn.Linear(input_dim, d_model), nn.LayerNorm(d_model), nn.Dropout(dropout) ) # Positional encoding self.pos_encoder = PositionalEncoding(d_model, max_seq_length, dropout) # Transformer encoder encoder_layer = nn.TransformerEncoderLayer( d_model=d_model, nhead=nhead, dim_feedforward=dim_feedforward, dropout=dropout, activation='gelu', batch_first=True ) self.transformer_encoder = nn.TransformerEncoder( encoder_layer, num_layers=num_encoder_layers ) # Classification head self.classifier = nn.Sequential( nn.Linear(d_model, d_model // 2), nn.LayerNorm(d_model // 2), nn.GELU(), nn.Dropout(dropout), nn.Linear(d_model // 2, num_classes) ) # Initialize weights self._init_weights() def _init_weights(self): """Initialize weights""" for p in self.parameters(): if p.dim() > 1: nn.init.xavier_uniform_(p) def forward(self, x: torch.Tensor, mask: Optional[torch.Tensor] = None) -> torch.Tensor: """ Forward pass Args: x: Input tensor of shape (batch_size, seq_len, input_dim) mask: Optional padding mask of shape (batch_size, seq_len) Returns: Output logits of shape (batch_size, num_classes) """ # Input projection x = self.input_projection(x) # (batch, seq_len, d_model) # Add positional encoding x = self.pos_encoder(x) # Create attention mask for padding if mask is not None: # Convert padding mask to attention mask # True values are masked (padding), False values are not masked attn_mask = mask.unsqueeze(1).unsqueeze(2) # (batch, 1, 1, seq_len) attn_mask = attn_mask.expand(-1, -1, x.size(1), -1) # (batch, 1, seq_len, seq_len) attn_mask = attn_mask.squeeze(1) # (batch, seq_len, seq_len) else: attn_mask = None # Transformer encoding x = self.transformer_encoder(x, src_key_padding_mask=mask) # (batch, seq_len, d_model) # Global average pooling over time dimension if mask is not None: # Mask out padding before pooling mask_expanded = (~mask).unsqueeze(-1).float() # (batch, seq_len, 1) x = (x * mask_expanded).sum(dim=1) / mask_expanded.sum(dim=1) # (batch, d_model) else: x = x.mean(dim=1) # (batch, d_model) # Classification logits = self.classifier(x) # (batch, num_classes) return logits def get_attention_weights(self, x: torch.Tensor, mask: Optional[torch.Tensor] = None): """Get attention weights for visualization""" # This is a simplified version - full implementation would require # modifying the transformer encoder to return attention weights with torch.no_grad(): x = self.input_projection(x) x = self.pos_encoder(x) # Note: Standard PyTorch transformer doesn't return attention weights # You would need to implement a custom version for this return None class LSTMModel(nn.Module): """LSTM baseline model for comparison""" def __init__(self, input_dim: int = 258, hidden_dim: int = 256, num_layers: int = 2, dropout: float = 0.3, num_classes: int = 100, bidirectional: bool = True): """ Args: input_dim: Dimension of input landmarks hidden_dim: Hidden dimension of LSTM num_layers: Number of LSTM layers dropout: Dropout rate num_classes: Number of output classes bidirectional: Whether to use bidirectional LSTM """ super().__init__() self.input_dim = input_dim self.hidden_dim = hidden_dim self.num_layers = num_layers self.bidirectional = bidirectional # LSTM self.lstm = nn.LSTM( input_size=input_dim, hidden_size=hidden_dim, num_layers=num_layers, dropout=dropout if num_layers > 1 else 0, bidirectional=bidirectional, batch_first=True ) # Classification head lstm_output_dim = hidden_dim * 2 if bidirectional else hidden_dim self.classifier = nn.Sequential( nn.Linear(lstm_output_dim, hidden_dim), nn.LayerNorm(hidden_dim), nn.ReLU(), nn.Dropout(dropout), nn.Linear(hidden_dim, num_classes) ) def forward(self, x: torch.Tensor, mask: Optional[torch.Tensor] = None) -> torch.Tensor: """ Forward pass Args: x: Input tensor of shape (batch_size, seq_len, input_dim) mask: Optional padding mask Returns: Output logits of shape (batch_size, num_classes) """ # LSTM lstm_out, (h_n, c_n) = self.lstm(x) # lstm_out: (batch, seq_len, hidden_dim * num_directions) # Use last hidden state if self.bidirectional: # Concatenate forward and backward hidden states hidden = torch.cat([h_n[-2], h_n[-1]], dim=1) # (batch, hidden_dim * 2) else: hidden = h_n[-1] # (batch, hidden_dim) # Classification logits = self.classifier(hidden) # (batch, num_classes) return logits def create_model(model_name: str = "transformer", **kwargs) -> nn.Module: """ Factory function to create models Args: model_name: Name of model ('transformer' or 'lstm') **kwargs: Model-specific arguments Returns: Model instance """ if model_name.lower() == "transformer": return MediaPipeTransformer(**kwargs) elif model_name.lower() == "lstm": return LSTMModel(**kwargs) else: raise ValueError(f"Unknown model: {model_name}")