Download model.py from lothnic/audiovec: direct link, hf CLI and curl.
- Browser
- Download file 10.9 kB
-
https://huggingface.co/lothnic/audiovec/resolve/main/model.py
- Command line
-
hf download hf://lothnic/audiovec/model.py
-
curl -L -o model.py https://huggingface.co/lothnic/audiovec/resolve/main/model.py
10.9 kB
| """Audio emotion embedding models (PyTorch). | |
| Architecture | |
| ------------ | |
| Two architectures are available: | |
| **Default β CRNN-Transformer** (recommended, 659K params): | |
| 3Γ Conv blocks β input projection β positional encoding β | |
| Transformer Encoder (2 layers, 4 heads) β mean-pool β embedding β classifier | |
| **Legacy β AudiovecModel (BiGRU)** (2.1M params): | |
| 3Γ Conv blocks β BiGRU (2 layers) β mean-pool β embedding β classifier | |
| Input shape convention | |
| ---------------------- | |
| Training data arrives as numpy arrays in channels-last format (N, H, W, C). | |
| PyTorch expects channels-first (N, C, H, W). The :meth:`forward` method | |
| handles the permutation automatically. | |
| """ | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| class ConvBlock(nn.Module): | |
| """Conv2d β BatchNorm2d β ReLU β MaxPool2d β Dropout2d.""" | |
| def __init__( | |
| self, | |
| in_channels: int, | |
| out_channels: int, | |
| kernel_size: int = 3, | |
| pool_size: int = 2, | |
| dropout: float = 0.0, | |
| ): | |
| super().__init__() | |
| self.conv = nn.Conv2d(in_channels, out_channels, kernel_size, padding=1) | |
| self.bn = nn.BatchNorm2d(out_channels) | |
| self.pool = nn.MaxPool2d(pool_size) | |
| self.dropout = nn.Dropout2d(dropout) if dropout > 0 else nn.Identity() | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| return self.dropout(self.pool(F.relu(self.bn(self.conv(x))))) | |
| class AudiovecModel(nn.Module): | |
| """CRNN that maps mel-spectrograms to emotion logits or embeddings. | |
| Parameters | |
| ---------- | |
| input_shape : | |
| (height, width, channels) in channels-last convention, e.g. (128, 228, 1). | |
| embedding_dim : | |
| Size of the embedding vector (default 256). | |
| num_classes : | |
| Number of emotion classes in the classifier head. | |
| dropout_rates : | |
| Dropout probabilities: (conv1, conv2, conv3, rnn_dropout, fc_dropout). | |
| """ | |
| def __init__( | |
| self, | |
| input_shape: tuple[int, int, int], | |
| embedding_dim: int = 256, | |
| num_classes: int = 8, | |
| dropout_rates: tuple[float, ...] = (0.05, 0.05, 0.1, 0.3, 0.3), | |
| ): | |
| super().__init__() | |
| h, w, c = input_shape | |
| # Pad dropout_rates to at least 5 entries | |
| if len(dropout_rates) < 5: | |
| dropout_rates = dropout_rates + (0.0,) * (5 - len(dropout_rates)) | |
| # ββ Convolutional feature extractor ββββββββββββββββββββββββββββββββ | |
| self.block1 = ConvBlock(c, 32, dropout=dropout_rates[0]) | |
| self.block2 = ConvBlock(32, 64, dropout=dropout_rates[1]) | |
| self.block3 = ConvBlock(64, 128, dropout=dropout_rates[2]) | |
| # Compute RNN input dimensions via dummy forward | |
| with torch.no_grad(): | |
| x = torch.zeros(1, c, h, w) | |
| x = self.block1(x) | |
| x = self.block2(x) | |
| x = self.block3(x) | |
| _, c_out, h_out, w_out = x.shape | |
| rnn_input_size = c_out * h_out # features per time step | |
| self._seq_len = w_out | |
| # ββ Bidirectional GRU ββββββββββββββββββββββββββββββββββββββββββββββ | |
| rnn_drop = dropout_rates[3] | |
| self.gru = nn.GRU( | |
| rnn_input_size, | |
| 128, | |
| num_layers=2, | |
| bidirectional=True, | |
| batch_first=True, | |
| dropout=rnn_drop if rnn_drop > 0 else 0, | |
| ) | |
| # ββ Embedding + classifier head ββββββββββββββββββββββββββββββββββββ | |
| fc_drop = dropout_rates[4] | |
| self.fc_dropout = nn.Dropout(fc_drop) if fc_drop > 0 else nn.Identity() | |
| self.fc_embed = nn.Linear(256, embedding_dim) # BiGRU output = 128*2 | |
| self.fc_classifier = nn.Linear(embedding_dim, num_classes) | |
| # Metadata | |
| self.embedding_dim = embedding_dim | |
| self.num_classes = num_classes | |
| def forward( | |
| self, | |
| x: torch.Tensor, | |
| return_embedding: bool = False, | |
| ) -> torch.Tensor: | |
| """Forward pass. | |
| Parameters | |
| ---------- | |
| x : | |
| Input tensor of shape (N, H, W, C) in channels-last convention. | |
| return_embedding : | |
| If True, return the embedding instead of class logits. | |
| Returns | |
| ------- | |
| torch.Tensor | |
| Classification logits (N, num_classes) or embeddings (N, embedding_dim). | |
| """ | |
| # Channels-last β channels-first | |
| x = x.permute(0, 3, 1, 2) | |
| # Convolutional blocks | |
| x = self.block1(x) # (N, 32, H/2, W/2) | |
| x = self.block2(x) # (N, 64, H/4, W/4) | |
| x = self.block3(x) # (N, 128, H/8, W/8) | |
| # Reshape for RNN: (N, C, H', W') β (N, W', C*H') | |
| N, C, H, W = x.shape | |
| x = x.permute(0, 3, 1, 2).reshape(N, W, C * H) # (N, seq_len, features) | |
| # Bidirectional GRU | |
| x, _ = self.gru(x) # (N, seq_len, 256) | |
| # Mean-pool over the time axis | |
| x = x.mean(dim=1) # (N, 256) | |
| # Dropout β embedding β classifier | |
| x = self.fc_dropout(x) | |
| embedding = F.relu(self.fc_embed(x)) | |
| if return_embedding: | |
| return embedding | |
| return self.fc_classifier(embedding) | |
| def predict_embedding(self, x: torch.Tensor) -> torch.Tensor: | |
| """Convenience: forward pass returning the embedding vector.""" | |
| self.eval() | |
| return self.forward(x, return_embedding=True) | |
| def predict_proba(self, x: torch.Tensor) -> torch.Tensor: | |
| """Convenience: forward pass returning softmax probabilities.""" | |
| self.eval() | |
| logits = self.forward(x, return_embedding=False) | |
| return F.softmax(logits, dim=-1) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # CRNN-Transformer (default architecture) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class PositionalEncoding(nn.Module): | |
| """Learned positional encoding for the transformer.""" | |
| def __init__(self, d_model: int, max_len: int = 60): | |
| super().__init__() | |
| self.pe = nn.Parameter(torch.randn(1, max_len, d_model) * 0.02) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| return x + self.pe[:, :x.size(1), :] | |
| class CRNNTransformer(nn.Module): | |
| """3Γ Conv β Transformer Encoder (2 layers, 4 heads) β mean-pool β embedding β classifier. | |
| This is the default architecture β best accuracy (72.92%) with only 659K params. | |
| """ | |
| def __init__( | |
| self, | |
| input_shape: tuple[int, int, int], | |
| embedding_dim: int = 256, | |
| num_classes: int = 8, | |
| dropout_rates: tuple[float, ...] = (0.05, 0.05, 0.1, 0.3, 0.3), | |
| ): | |
| super().__init__() | |
| h, w, c = input_shape | |
| dr = dropout_rates if len(dropout_rates) >= 5 else dropout_rates + (0.0,) * (5 - len(dropout_rates)) | |
| self.blocks = nn.ModuleList([ | |
| ConvBlock(c, 32, dropout=dr[0]), | |
| ConvBlock(32, 64, dropout=dr[1]), | |
| ConvBlock(64, 128, dropout=dr[2]), | |
| ]) | |
| with torch.no_grad(): | |
| x = torch.zeros(1, c, h, w) | |
| for b in self.blocks: | |
| x = b(x) | |
| _, c_out, h_out, w_out = x.shape | |
| self._seq_len = w_out | |
| self._feat_dim = c_out * h_out | |
| d_model = 128 # matches GRU hidden dim | |
| nhead = 4 | |
| num_layers = 2 | |
| self.input_proj = nn.Linear(self._feat_dim, d_model) | |
| self.pos_encoding = PositionalEncoding(d_model, max_len=self._seq_len) | |
| encoder_layer = nn.TransformerEncoderLayer( | |
| d_model=d_model, | |
| nhead=nhead, | |
| dim_feedforward=d_model * 2, | |
| dropout=dr[3], | |
| activation="gelu", | |
| batch_first=True, | |
| norm_first=True, # pre-norm for stable training | |
| ) | |
| self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) | |
| self.drop = nn.Dropout(dr[4]) if dr[4] > 0 else nn.Identity() | |
| self.fc_embed = nn.Linear(d_model, embedding_dim) | |
| self.fc_cls = nn.Linear(embedding_dim, num_classes) | |
| self.embedding_dim = embedding_dim | |
| self.num_classes = num_classes | |
| def forward(self, x: torch.Tensor, return_embedding: bool = False) -> torch.Tensor: | |
| x = x.permute(0, 3, 1, 2) | |
| for b in self.blocks: | |
| x = b(x) | |
| N, C, H, W = x.shape | |
| x = x.permute(0, 3, 1, 2).reshape(N, W, C * H) | |
| x = self.input_proj(x) | |
| x = self.pos_encoding(x) | |
| x = self.transformer(x) | |
| x = x.mean(dim=1) | |
| x = self.drop(x) | |
| emb = F.relu(self.fc_embed(x)) | |
| return emb if return_embedding else self.fc_cls(emb) | |
| def predict_embedding(self, x: torch.Tensor) -> torch.Tensor: | |
| self.eval() | |
| return self.forward(x, return_embedding=True) | |
| def predict_proba(self, x: torch.Tensor) -> torch.Tensor: | |
| self.eval() | |
| return F.softmax(self.forward(x), dim=-1) | |
| def build_model( | |
| input_shape: tuple[int, int, int], | |
| embedding_dim: int = 256, | |
| num_classes: int = 8, | |
| dropout_rates: tuple[float, ...] = (0.05, 0.05, 0.1, 0.3, 0.3), | |
| ) -> AudiovecModel: | |
| """Build the audiovec CRNN model (factory function). | |
| Parameters | |
| ---------- | |
| input_shape : | |
| (n_mels, time_steps, 1) β channels-last, e.g. (128, 228, 1). | |
| embedding_dim : | |
| Size of the embedding vector (default 256). | |
| num_classes : | |
| Number of emotion classes in the classifier head. | |
| dropout_rates : | |
| Dropout probabilities (3 conv + 1 rnn + 1 fc). | |
| Returns | |
| ------- | |
| AudiovecModel | |
| """ | |
| return AudiovecModel( | |
| input_shape=input_shape, | |
| embedding_dim=embedding_dim, | |
| num_classes=num_classes, | |
| dropout_rates=dropout_rates, | |
| ) | |
| def build_transformer_model( | |
| input_shape: tuple[int, int, int], | |
| embedding_dim: int = 256, | |
| num_classes: int = 8, | |
| dropout_rates: tuple[float, ...] = (0.05, 0.05, 0.1, 0.3, 0.3), | |
| ) -> CRNNTransformer: | |
| """Build the CRNN-Transformer model (factory function). | |
| This is the default architecture β best accuracy (72.92%) with 659K params. | |
| """ | |
| return CRNNTransformer( | |
| input_shape=input_shape, | |
| embedding_dim=embedding_dim, | |
| num_classes=num_classes, | |
| dropout_rates=dropout_rates, | |
| ) |