| import torch |
| import torch.nn as nn |
| from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence |
|
|
|
|
| class BiLSTMFeatureExtractor(nn.Module): |
| """ |
| BiLSTM Feature Extractor module. |
| |
| Inputs: |
| x : Padded WavLM sequence embeddings [B, T, 768] |
| mask : Timestep mask [B, T] (optional, 1 for real timesteps, 0 for padding) |
| |
| Outputs: |
| out : BiLSTM contextual representations [B, T, 256] |
| """ |
| def __init__(self, input_size=768, hidden_size=128, num_layers=1, dropout=0.0): |
| super(BiLSTMFeatureExtractor, self).__init__() |
| |
| self.input_size = input_size |
| self.hidden_size = hidden_size |
| self.num_layers = num_layers |
| self.bidirectional = True |
| |
| |
| self.bilstm = nn.LSTM( |
| input_size=input_size, |
| hidden_size=hidden_size, |
| num_layers=num_layers, |
| batch_first=True, |
| bidirectional=True, |
| dropout=dropout if num_layers > 1 else 0.0 |
| ) |
| |
| def forward(self, x, mask=None): |
| batch_size, seq_len, _ = x.shape |
| |
| if mask is not None: |
| lengths = mask.sum(dim=1).cpu() |
| |
| packed_x = pack_padded_sequence( |
| x, |
| lengths, |
| batch_first=True, |
| enforce_sorted=False |
| ) |
| |
| packed_out, (hn, cn) = self.bilstm(packed_x) |
| |
| out, _ = pad_packed_sequence( |
| packed_out, |
| batch_first=True, |
| total_length=seq_len |
| ) |
| else: |
| out, (hn, cn) = self.bilstm(x) |
| |
| return out |
|
|
|
|
| class TemporalAttention(nn.Module): |
| """ |
| Temporal Attention Module. |
| Learns frame-wise attention scores over sequence length T to aggregate |
| BiLSTM output sequence [B, T, 256] into a fixed 256-dimensional context vector [B, 256]. |
| |
| Inputs: |
| h : BiLSTM outputs [B, T, input_dim] (input_dim=256) |
| mask : Timestep mask [B, T] (1 for real timesteps, 0 for padding) |
| |
| Outputs: |
| context : Aggregated context vector [B, 256] |
| attn_weights : Normalized attention weights [B, T] |
| """ |
| def __init__(self, input_dim=256): |
| super(TemporalAttention, self).__init__() |
| self.input_dim = input_dim |
| self.w = nn.Linear(input_dim, 1, bias=False) |
| |
| def forward(self, h, mask=None): |
| scores = self.w(torch.tanh(h)).squeeze(-1) |
| |
| if mask is not None: |
| scores = scores.masked_fill(mask == 0, -1e9) |
| |
| attn_weights = torch.softmax(scores, dim=1) |
| context = torch.bmm(attn_weights.unsqueeze(1), h).squeeze(1) |
| |
| return context, attn_weights |
|
|
|
|
| class BiLSTMAttentionClassifier(nn.Module): |
| """ |
| Full Downstream Model Architecture: |
| WavLM Embedding [B, T, 768] -> BiLSTM -> [B, T, 256] -> Temporal Attention -> [B, 256] -> Linear Classifier -> [B, 6] Logits |
| """ |
| def __init__(self, input_size=768, hidden_size=128, num_classes=6, dropout=0.3): |
| super(BiLSTMAttentionClassifier, self).__init__() |
| |
| self.bilstm = BiLSTMFeatureExtractor( |
| input_size=input_size, |
| hidden_size=hidden_size, |
| num_layers=1, |
| dropout=dropout |
| ) |
| |
| context_dim = hidden_size * 2 |
| self.attention = TemporalAttention(input_dim=context_dim) |
| |
| self.dropout = nn.Dropout(dropout) |
| self.classifier = nn.Linear(context_dim, num_classes) |
| |
| def forward(self, x, mask=None): |
| |
| bilstm_out = self.bilstm(x, mask=mask) |
| |
| |
| context, attn_weights = self.attention(bilstm_out, mask=mask) |
| |
| |
| dropped_context = self.dropout(context) |
| logits = self.classifier(dropped_context) |
| |
| return logits, attn_weights |
|
|