实现了一个基于
Conformer
架构的说话人分类系统,主要包含以下内容:
- 数据处理部分:
- 使用VoxCeleb2数据集中的600个说话人
- 预处理音频为梅尔频谱图
- 构建数据加载器并实现分段处理
- 模型架构:
- 实现Conformer模块,包含:位置编码、相对多头注意力、前馈网络等组件
- 支持自注意力池化(Self-Attention Pooling)和AMSoftmax损失函数
- 采用学习率预热和余弦退火调度
- 训练流程:
- 支持从预训练模型加载
- 使用AdamW优化器
- 每2000步进行验证并保存最佳模型
该系统可实现说话人分类任务,通过调整参数和模型组件可以优化性能。代码结构清晰,包含数据加载、模型定义、训练和推理的完整流程。
Boss Baseline
You can search for the “Boss” tag to check for changes (Ctrl + F or
Command
+ F)
Task description
- Classify the speakers of given features.
- Main goal: Learn how to use transformer.
- Baselines:
- Easy: Run sample code and know how to use transformer.
- Medium: Know how to adjust parameters of transformer.
- Strong: Construct conformer which is a variety of transformer.
- Boss: Implement Self-Attention Pooling & Additive Margin Softmax to further boost the performance.
- Other links
- Competiton: link
- Slide: link
- Data: link
# !wget https://github.com/googly-mingto/ML2023HW4/releases/download/data/Dataset.tar.gz.partaa # !wget https://github.com/googly-mingto/ML2023HW4/releases/download/data/Dataset.tar.gz.partab # !wget https://github.com/googly-mingto/ML2023HW4/releases/download/data/Dataset.tar.gz.partac # !wget https://github.com/googly-mingto/ML2023HW4/releases/download/data/Dataset.tar.gz.partad # !cat Dataset.tar.gz.part* > Dataset.tar.gz # !rm Dataset.tar.gz.partaa # !rm Dataset.tar.gz.partab # !rm Dataset.tar.gz.partac # !rm Dataset.tar.gz.partad # # unzip the file # !tar zxf Dataset.tar.gz # !rm Dataset.tar.gz
# !tar zxf Dataset.tar.gz
import numpy as np import torch import random def set_seed(seed): np.random.seed(seed) random.seed(seed) torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.benchmark = False torch.backends.cudnn.deterministic = True set_seed(87)
Data
Dataset
- Original dataset is Voxceleb2.
- The license and complete version of Voxceleb2.
- We randomly select 600 speakers from Voxceleb2.
- Then preprocess the raw waveforms into mel-spectrograms.
- Args:
- data_dir: The path to the data directory.
- metadata_path: The path to the metadata.
- segment_len: The length of audio segment for training.
- The architecture of data directory
- data directory |---- metadata.json |---- testdata.json |---- mapping.json |---- uttr-{random string}.pt
- The information in metadata
- "n_mels": The dimention of mel-spectrogram.
- "speakers": A dictionary.
- Key: speaker ids.
- value: "feature_path" and "mel_len"
For efficiency, we segment the mel-spectrograms into segments in the traing step.
import os import json import torch import random from pathlib import Path from torch.utils.data import Dataset from torch.nn.utils.rnn import pad_sequence class myDataset(Dataset): def __init__(self, data_dir, segment_len=128): self.data_dir = data_dir self.segment_len = segment_len # Load the mapping from speaker neme to their corresponding id. mapping_path = Path(data_dir) / "mapping.json" mapping = json.load(mapping_path.open()) self.speaker2id = mapping["speaker2id"] # Load metadata of training data. metadata_path = Path(data_dir) / "metadata.json" metadata = json.load(open(metadata_path))["speakers"] # Get the total number of speaker. self.speaker_num = len(metadata.keys()) self.data = [] for speaker in metadata.keys(): for utterances in metadata[speaker]: self.data.append([utterances["feature_path"], self.speaker2id[speaker]]) def __len__(self): return len(self.data) def __getitem__(self, index): feat_path, speaker = self.data[index] # Load preprocessed mel-spectrogram. mel = torch.load(os.path.join(self.data_dir, feat_path)) # Segment mel-spectrogram into "segment_len" frames. if len(mel) > self.segment_len: # Randomly get the starting point of the segment. start = random.randint(0, len(mel) - self.segment_len) # Get a segment with "segment_len" frames. mel = torch.FloatTensor(mel[start:start+self.segment_len]) else: mel = torch.FloatTensor(mel) # Turn the speaker id into long for computing loss later. speaker = torch.FloatTensor([speaker]).long() # .long().dtype = torch.int64 return mel, speaker def get_speaker_number(self): return self.speaker_num
Dataloader
- Split dataset into training dataset(90%) and validation dataset(10%).
- Create dataloader to iterate the data.
import torch from torch.utils.data import DataLoader, random_split from torch.nn.utils.rnn import pad_sequence def collate_batch(batch): # Process features within a batch. """Collate a batch of data.""" mel, speaker = zip(*batch) # Because we train the model batch by batch, we need to pad the features in the same batch to make their lengths the same. mel = pad_sequence(mel, batch_first=True, padding_value=-20) # pad log 10^(-20) which is very small value. # mel: (batch size, length, 40) return mel, torch.FloatTensor(speaker).long() def get_dataloader(data_dir, batch_size, n_workers): """Generate dataloader""" dataset = myDataset(data_dir) speaker_num = dataset.get_speaker_number() # Split dataset into training dataset and validation dataset trainlen = int(0.9 * len(dataset)) lengths = [trainlen, len(dataset) - trainlen] trainset, validset = random_split(dataset, lengths) train_loader = DataLoader( trainset, batch_size=batch_size, shuffle=True, drop_last=True, num_workers=n_workers, pin_memory=True, collate_fn=collate_batch, ) valid_loader = DataLoader( validset, batch_size=batch_size, num_workers=n_workers, drop_last=True, pin_memory=True, collate_fn=collate_batch, ) return train_loader, valid_loader, speaker_num
Model
- TransformerEncoderLayer:
- Base transformer encoder layer in Attention Is All You Need
- Parameters:
- d_model: the number of expected features of the input (required).
- nhead: the number of heads of the multiheadattention models (required).
- dim_feedforward: the dimension of the feedforward network model (default=2048).
- dropout: the dropout value (default=0.1).
- activation: the activation function of intermediate layer, relu or gelu (default=relu).
- TransformerEncoder:
- TransformerEncoder is a stack of N transformer encoder layers
- Parameters:
- encoder_layer: an instance of the TransformerEncoderLayer() class (required).
- num_layers: the number of sub-encoder-layers in the encoder (required).
- norm: the layer normalization component (optional).
Conformer
The GitHub source code for the Conformer paper has been included in
this
notebook for reference.
Encoder model architecture

Basic
Modules
This
cell
contains implementations of several basic modules that will be used to construct the Conformer model architecture in the following sections:
ResidualConnectionModule: Implements a residual connection module
Linear: Wrapper fortorch.nn.Linearwith Xavier initialization
View: Wrapper fortorch.view()to be used in Sequential
Transpose: Wrapper fortorch.transpose()to be used in Sequential
Swish: Implementation of the Swish activation function
GLU: Implementation of Gated Linear Units
These modules provide basic building
blocks
like linear transformations, activations, residual connections that will be used to build components of the Conformer model such as the multi-headed attention and feedforward layers.
# Copyright (c) 2021, Soohwan Kim. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import torch.nn as nn import torch.nn.init as init from torch import Tensor from typing import Tuple class ResidualConnectionModule(nn.Module): """ Residual Connection Module. outputs = (module(inputs) x module_factor + inputs x input_factor) """ def __init__(self, module: nn.Module, module_factor: float = 1.0, input_factor: float = 1.0): super(ResidualConnectionModule, self).__init__() self.module = module self.module_factor = module_factor self.input_factor = input_factor def forward(self, inputs: Tensor) -> Tensor: return (self.module(inputs) * self.module_factor) + (inputs * self.input_factor) class Linear(nn.Module): """ Wrapper class of torch.nn.Linear Weight initialize by xavier initialization and bias initialize to zeros. """ def __init__(self, in_features: int, out_features: int, bias: bool = True) -> None: super(Linear, self).__init__() self.linear = nn.Linear(in_features, out_features, bias=bias) init.xavier_uniform_(self.linear.weight) if bias: init.zeros_(self.linear.bias) def forward(self, x: Tensor) -> Tensor: return self.linear(x) class View(nn.Module): """ Wrapper class of torch.view() for Sequential module. """ def __init__(self, shape: tuple, contiguous: bool = False): super(View, self).__init__() self.shape = shape self.contiguous = contiguous def forward(self, x: Tensor) -> Tensor: if self.contiguous: x = x.contiguous() return x.view(*self.shape) class Transpose(nn.Module): """ Wrapper class of torch.transpose() for Sequential module. """ def __init__(self, shape: tuple): super(Transpose, self).__init__() self.shape = shape def forward(self, x: Tensor) -> Tensor: return x.transpose(*self.shape) class Swish(nn.Module): """ Swish is a smooth, non-monotonic function that consistently matches or outperforms ReLU on deep networks applied to a variety of challenging domains such as Image classification and Machine translation. """ def __init__(self): super(Swish, self).__init__() def forward(self, inputs: Tensor) -> Tensor: return inputs * inputs.sigmoid() class GLU(nn.Module): """ The gating mechanism is called Gated Linear Units (GLU), which was first introduced for natural language processing in the paper “Language Modeling with Gated Convolutional Networks” """ def __init__(self, dim: int) -> None: super(GLU, self).__init__() self.dim = dim def forward(self, inputs: Tensor) -> Tensor: outputs, gate = inputs.chunk(2, dim=self.dim) return outputs * gate.sigmoid()
Feed Forward Module
This cell implements the Feed Forward Module for the Conformer model.
Key points:
- It follows the pre-norm residual unit design, with layer normalization applied before the first linear layer
- Two linear layers with Swish activation in between
- Expansion factor of 4 on the hidden layer dimensionality
- Dropout applied after both linear layers for regularization
class FeedForwardModule(nn.Module): """ Conformer Feed Forward Module follow pre-norm residual units and apply layer normalization within the residual unit and on the input before the first linear layer. This module also apply Swish activation and dropout, which helps regularizing the network. Args: encoder_dim (int): Dimension of conformer encoder expansion_factor (int): Expansion factor of feed forward module. dropout_p (float): Ratio of dropout Inputs: inputs - **inputs** (batch, time, dim): Tensor contains input sequences Outputs: outputs - **outputs** (batch, time, dim): Tensor produces by feed forward module. """ def __init__( self, encoder_dim: int = 512, expansion_factor: int = 4, dropout_p: float = 0.1, ) -> None: super(FeedForwardModule, self).__init__() self.sequential = nn.Sequential( nn.LayerNorm(encoder_dim), Linear(encoder_dim, encoder_dim * expansion_factor, bias=True), Swish(), nn.Dropout(p=dropout_p), Linear(encoder_dim * expansion_factor, encoder_dim, bias=True), nn.Dropout(p=dropout_p), ) def forward(self, inputs: Tensor) -> Tensor: return self.sequential(inputs)
Positional Encoding
class PositionalEncoding(nn.Module): """ Positional Encoding proposed in "Attention Is All You Need". Since transformer contains no recurrence and no convolution, in order for the model to make use of the order of the sequence, we must add some positional information. "Attention Is All You Need" use sine and cosine functions of different frequencies: PE_(pos, 2i) = sin(pos / power(10000, 2i / d_model)) PE_(pos, 2i+1) = cos(pos / power(10000, 2i / d_model)) """ def __init__(self, d_model: int = 512, max_len: int = 10000) -> None: super(PositionalEncoding, self).__init__() pe = torch.zeros(max_len, d_model, requires_grad=False) 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, length: int) -> Tensor: return self.pe[:, :length]
Relative Multi-Head Attention
This module implements relative multi-head attention mechanism as proposed in Transformer-XL paper.
Key points:
- Splits input into multiple heads, applies attention separately on each head
- Adds positional embedding to the query before calculating attention score
- Positional embeddings are also projected to number of heads
- Attention scores calculated using both content-based and position-based similarities
- Relative positional shifts are applied to the position embeddings
- Context vectors from each head are concatenated and projected
from typing import Optional class RelativeMultiHeadAttention(nn.Module): """ Multi-head attention with relative positional encoding. This concept was proposed in the "Transformer-XL: Attentive Language Models Beyond a Fixed-Length Context" Args: d_model (int): The dimension of model num_heads (int): The number of attention heads. dropout_p (float): probability of dropout Inputs: query, key, value, pos_embedding, mask - **query** (batch, time, dim): Tensor containing query vector - **key** (batch, time, dim): Tensor containing key vector - **value** (batch, time, dim): Tensor containing value vector - **pos_embedding** (batch, time, dim): Positional embedding tensor - **mask** (batch, 1, time2) or (batch, time1, time2): Tensor containing indices to be masked Returns: - **outputs**: Tensor produces by relative multi head attention module. """ def __init__( self, d_model: int = 512, num_heads: int = 16, dropout_p: float = 0.1, ): super(RelativeMultiHeadAttention, self).__init__() assert d_model % num_heads == 0, "d_model % num_heads should be zero." self.d_model = d_model self.d_head = int(d_model / num_heads) self.num_heads = num_heads self.sqrt_dim = math.sqrt(d_model) self.query_proj = Linear(d_model, d_model) self.key_proj = Linear(d_model, d_model) self.value_proj = Linear(d_model, d_model) self.pos_proj = Linear(d_model, d_model, bias=False) self.dropout = nn.Dropout(p=dropout_p) self.u_bias = nn.Parameter(torch.Tensor(self.num_heads, self.d_head)) self.v_bias = nn.Parameter(torch.Tensor(self.num_heads, self.d_head)) torch.nn.init.xavier_uniform_(self.u_bias) torch.nn.init.xavier_uniform_(self.v_bias) self.out_proj = Linear(d_model, d_model) def forward( self, query: Tensor, key: Tensor, value: Tensor, pos_embedding: Tensor, mask: Optional[Tensor] = None, ) -> Tensor: batch_size = value.size(0) query = self.query_proj(query).view(batch_size, -1, self.num_heads, self.d_head) key = self.key_proj(key).view(batch_size, -1, self.num_heads, self.d_head).permute(0, 2, 1, 3) value = self.value_proj(value).view(batch_size, -1, self.num_heads, self.d_head).permute(0, 2, 1, 3) pos_embedding = self.pos_proj(pos_embedding).view(batch_size, -1, self.num_heads, self.d_head) content_score = torch.matmul((query + self.u_bias).transpose(1, 2), key.transpose(2, 3)) pos_score = torch.matmul((query + self.v_bias).transpose(1, 2), pos_embedding.permute(0, 2, 3, 1)) pos_score = self._relative_shift(pos_score) score = (content_score + pos_score) / self.sqrt_dim if mask is not None: mask = mask.unsqueeze(1) score.masked_fill_(mask, -1e9) attn = F.softmax(score, -1) attn = self.dropout(attn) context = torch.matmul(attn, value).transpose(1, 2) context = context.contiguous().view(batch_size, -1, self.d_model) return self.out_proj(context) def _relative_shift(self, pos_score: Tensor) -> Tensor: batch_size, num_heads, seq_length1, seq_length2 = pos_score.size() zeros = pos_score.new_zeros(batch_size, num_heads, seq_length1, 1) padded_pos_score = torch.cat([zeros, pos_score], dim=-1) padded_pos_score = padded_pos_score.view(batch_size, num_heads, seq_length2 + 1, seq_length1) pos_score = padded_pos_score[:, :, 1:].view_as(pos_score) return pos_score
Multi-Headed Self Attention Module
class MultiHeadedSelfAttentionModule(nn.Module): """ Conformer employ multi-headed self-attention (MHSA) while integrating an important technique from Transformer-XL, the relative sinusoidal positional encoding scheme. The relative positional encoding allows the self-attention module to generalize better on different input length and the resulting encoder is more robust to the variance of the utterance length. Conformer use prenorm residual units with dropout which helps training and regularizing deeper models. Args: d_model (int): The dimension of model num_heads (int): The number of attention heads. dropout_p (float): probability of dropout Inputs: inputs, mask - **inputs** (batch, time, dim): Tensor containing input vector - **mask** (batch, 1, time2) or (batch, time1, time2): Tensor containing indices to be masked Returns: - **outputs** (batch, time, dim): Tensor produces by relative multi headed self attention module. """ def __init__(self, d_model: int, num_heads: int, dropout_p: float = 0.1): super(MultiHeadedSelfAttentionModule, self).__init__() self.positional_encoding = PositionalEncoding(d_model) self.layer_norm = nn.LayerNorm(d_model) self.attention = RelativeMultiHeadAttention(d_model, num_heads, dropout_p) self.dropout = nn.Dropout(p=dropout_p) def forward(self, inputs: Tensor, mask: Optional[Tensor] = None): batch_size, seq_length, _ = inputs.size() pos_embedding = self.positional_encoding(seq_length) pos_embedding = pos_embedding.repeat(batch_size, 1, 1) inputs = self.layer_norm(inputs) outputs = self.attention(inputs, inputs, inputs, pos_embedding=pos_embedding, mask=mask) return self.dropout(outputs)
Conformer Convolution Module

class DepthwiseConv1d(nn.Module): """ When groups == in_channels and out_channels == K * in_channels, where K is a positive integer, this operation is termed in literature as depthwise convolution. Args: in_channels (int): Number of channels in the input out_channels (int): Number of channels produced by the convolution kernel_size (int or tuple): Size of the convolving kernel stride (int, optional): Stride of the convolution. Default: 1 padding (int or tuple, optional): Zero-padding added to both sides of the input. Default: 0 bias (bool, optional): If True, adds a learnable bias to the output. Default: True Inputs: inputs - **inputs** (batch, in_channels, time): Tensor containing input vector Returns: outputs - **outputs** (batch, out_channels, time): Tensor produces by depthwise 1-D convolution. """ def __init__( self, in_channels: int, out_channels: int, kernel_size: int, stride: int = 1, padding: int = 0, bias: bool = False, ) -> None: super(DepthwiseConv1d, self).__init__() assert out_channels % in_channels == 0, "out_channels should be constant multiple of in_channels" self.conv = nn.Conv1d( in_channels=in_channels, out_channels=out_channels, kernel_size=kernel_size, groups=in_channels, stride=stride, padding=padding, bias=bias, ) def forward(self, inputs: Tensor) -> Tensor: return self.conv(inputs) class PointwiseConv1d(nn.Module): """ When kernel size == 1 conv1d, this operation is termed in literature as pointwise convolution. This operation often used to match dimensions. Args: in_channels (int): Number of channels in the input out_channels (int): Number of channels produced by the convolution stride (int, optional): Stride of the convolution. Default: 1 padding (int or tuple, optional): Zero-padding added to both sides of the input. Default: 0 bias (bool, optional): If True, adds a learnable bias to the output. Default: True Inputs: inputs - **inputs** (batch, in_channels, time): Tensor containing input vector Returns: outputs - **outputs** (batch, out_channels, time): Tensor produces by pointwise 1-D convolution. """ def __init__( self, in_channels: int, out_channels: int, stride: int = 1, padding: int = 0, bias: bool = True, ) -> None: super(PointwiseConv1d, self).__init__() self.conv = nn.Conv1d( in_channels=in_channels, out_channels=out_channels, kernel_size=1, stride=stride, padding=padding, bias=bias, ) def forward(self, inputs: Tensor) -> Tensor: return self.conv(inputs) class ConformerConvModule(nn.Module): """ Conformer convolution module starts with a pointwise convolution and a gated linear unit (GLU). This is followed by a single 1-D depthwise convolution layer. Batchnorm is deployed just after the convolution to aid training deep models. Args: in_channels (int): Number of channels in the input kernel_size (int or tuple, optional): Size of the convolving kernel Default: 31 dropout_p (float, optional): probability of dropout Inputs: inputs inputs (batch, time, dim): Tensor contains input sequences Outputs: outputs outputs (batch, time, dim): Tensor produces by conformer convolution module. """ def __init__( self, in_channels: int, kernel_size: int = 31, expansion_factor: int = 2, dropout_p: float = 0.1, ) -> None: super(ConformerConvModule, self).__init__() assert (kernel_size - 1) % 2 == 0, "kernel_size should be a odd number for 'SAME' padding" assert expansion_factor == 2, "Currently, Only Supports expansion_factor 2" self.sequential = nn.Sequential( nn.LayerNorm(in_channels), Transpose(shape=(1, 2)), PointwiseConv1d(in_channels, in_channels * expansion_factor, stride=1, padding=0, bias=True), GLU(dim=1), DepthwiseConv1d(in_channels, in_channels, kernel_size, stride=1, padding=(kernel_size - 1) // 2), nn.BatchNorm1d(in_channels), Swish(), PointwiseConv1d(in_channels, in_channels, stride=1, padding=0, bias=True), nn.Dropout(p=dropout_p), ) def forward(self, inputs: Tensor) -> Tensor: return self.sequential(inputs).transpose(1, 2)
Conformer Block
The
ConformerBlock implements the main building block of the Conformer model.Key points:
- It sandwiches two FeedForward modules around the MultiHeadedSelfAttention module and the ConformerConv module
- Applying FeedForward before and after the attention is known as Macaron style blocks
- Residual connections are used around each module, with layer norm applied in the end
- Half-step residual connections are also supported
The block takes input, passes it through the feedforward -> attn -> conv -> feedforward blocks with residuals. This forms the basic
transformer
encoder layer used to stack up the Conformer model.
class ConformerBlock(nn.Module): """ Conformer block contains two Feed Forward modules sandwiching the Multi-Headed Self-Attention module and the Convolution module. This sandwich structure is inspired by Macaron-Net, which proposes replacing the original feed-forward layer in the Transformer block into two half-step feed-forward layers, one before the attention layer and one after. Args: encoder_dim (int, optional): Dimension of conformer encoder num_attention_heads (int, optional): Number of attention heads feed_forward_expansion_factor (int, optional): Expansion factor of feed forward module conv_expansion_factor (int, optional): Expansion factor of conformer convolution module feed_forward_dropout_p (float, optional): Probability of feed forward module dropout attention_dropout_p (float, optional): Probability of attention module dropout conv_dropout_p (float, optional): Probability of conformer convolution module dropout conv_kernel_size (int or tuple, optional): Size of the convolving kernel half_step_residual (bool): Flag indication whether to use half step residual or not Inputs: inputs - **inputs** (batch, time, dim): Tensor containing input vector Returns: outputs - **outputs** (batch, time, dim): Tensor produces by conformer block. """ def __init__( self, encoder_dim: int = 512, num_attention_heads: int = 8, feed_forward_expansion_factor: int = 4, conv_expansion_factor: int = 2, feed_forward_dropout_p: float = 0.1, attention_dropout_p: float = 0.1, conv_dropout_p: float = 0.1, conv_kernel_size: int = 31, half_step_residual: bool = True, ): super(ConformerBlock, self).__init__() if half_step_residual: self.feed_forward_residual_factor = 0.5 else: self.feed_forward_residual_factor = 1 self.sequential = nn.Sequential( ResidualConnectionModule( module=FeedForwardModule( encoder_dim=encoder_dim, expansion_factor=feed_forward_expansion_factor, dropout_p=feed_forward_dropout_p, ), module_factor=self.feed_forward_residual_factor, ), ResidualConnectionModule( module=MultiHeadedSelfAttentionModule( d_model=encoder_dim, num_heads=num_attention_heads, dropout_p=attention_dropout_p, ), ), ResidualConnectionModule( module=ConformerConvModule( in_channels=encoder_dim, kernel_size=conv_kernel_size, expansion_factor=conv_expansion_factor, dropout_p=conv_dropout_p, ), ), ResidualConnectionModule( module=FeedForwardModule( encoder_dim=encoder_dim, expansion_factor=feed_forward_expansion_factor, dropout_p=feed_forward_dropout_p, ), module_factor=self.feed_forward_residual_factor, ), nn.LayerNorm(encoder_dim), ) def forward(self, inputs: Tensor) -> Tensor: return self.sequential(inputs)
Classifier (Boss)
import torch import torch.nn as nn import torch.nn.functional as F class Classifier(nn.Module): def __init__(self, d_model=512, n_spks=600, dropout=0.1): super().__init__() # Project the dimension of features from that of input into d_model. self.prenet = nn.Linear(40, d_model) # TODO: # Change Transformer to Conformer. # https://arxiv.org/abs/2005.08100 self.encoder_layer = ConformerBlock(encoder_dim=d_model, conv_dropout_p=0.1) self.self_attention_pooling = SelfAttentionPooling(input_dim=d_model) # Project the the dimension of features from d_model into speaker nums. self.pred_layer = Linear(d_model, n_spks) # You can achieve boss baseline by just changing prediction layer def forward(self, mels): """ args: mels: (batch size, length, 40) return: out: (batch size, n_spks) """ # out: (batch size, length, d_model) out = self.prenet(mels) # out: (length, batch size, d_model) out = out.permute(1, 0, 2) # The encoder layer expect features in the shape of (length, batch size, d_model). out = self.encoder_layer(out) # out: (batch size, length, d_model) out = out.transpose(0, 1) # mean pooling stats = out.mean(dim=1) # self-attention pooling if SAP: stats = self.self_attention_pooling(out) out = self.pred_layer(stats) return out
Self-Attention Pooling (Boss)
class SelfAttentionPooling(nn.Module): """ Implementation of SelfAttentionPooling Original Paper: Self-Attention Encoding and Pooling for Speaker Recognition https://arxiv.org/pdf/2008.01077v1.pdf """ def __init__(self, input_dim): super(SelfAttentionPooling, self).__init__() self.W = nn.Linear(input_dim, 1) def forward(self, batch_rep): """ input: batch_rep : size (N, T, H), N: batch size, T: sequence length, H: Hidden dimension attention_weight: att_w : size (N, T, 1) return: utter_rep: size (N, H) """ softmax = nn.functional.softmax att_w = softmax(self.W(batch_rep).squeeze(-1), dim=-1).unsqueeze(-1) utter_rep = torch.sum(batch_rep * att_w, dim=1) return utter_rep
AMSoftmax (Boss)
from torch.autograd import Variable # https://github.com/zhilangtaosha/SpeakerVerification_AMSoftmax_pytorch/blob/63629989c152664c838823456532682585d72e31/model/model.py#L257 class AMSoftmax(nn.Module): def __init__(self): super(AMSoftmax, self).__init__() def forward(self, input, target, scale=10.0, margin=0.35): cos_theta = input target = target.view(-1, 1) # size=(B,1) index = cos_theta.data * 0.0 # size=(B,Classnum) index.scatter_(1, target.data.view(-1, 1), 1) index = index.byte() index = Variable(index) output = cos_theta * 1.0 # size=(B,Classnum) output[index.bool()] -= margin output = output * scale logpt = F.log_softmax(output, dim=1) logpt = logpt.gather(1, target) logpt = logpt.view(-1) loss = -1 * logpt loss = loss.mean() return loss # # Usage: https://github.com/CoinCheung/pytorch-loss/issues/16 # class AMSoftmax(nn.Module): # def __init__(self, in_feats, n_classes=10, m=0.35, s=30): # super(AMSoftmax, self).__init__() # self.m = m # self.s = s # self.in_feats = in_feats # self.W = torch.nn.Parameter(torch.randn(in_feats, n_classes), requires_grad=True) # self.ce = nn.CrossEntropyLoss() # nn.init.xavier_normal_(self.W, gain=1) # def forward(self, x, lb): # assert x.size()[0] == lb.size()[0] # assert x.size()[1] == self.in_feats # x_norm = torch.norm(x, p=2, dim=1, keepdim=True).clamp(min=1e-9) # x_norm = torch.div(x, x_norm) # w_norm = torch.norm(self.W, p=2, dim=0, keepdim=True).clamp(min=1e-9) # w_norm = torch.div(self.W, w_norm) # costh = torch.mm(x_norm, w_norm) # delt_costh = torch.zeros_like(costh).scatter_(1, lb.unsqueeze(1), self.m) # costh_m = costh - delt_costh # costh_m_s = self.s * costh_m # loss = self.ce(costh_m_s, lb) # return costh_m_s, loss
Learning rate schedule
- For transformer architecture, the design of learning rate schedule is different from that of CNN.
- Previous works show that the warmup of learning rate is useful for training models with transformer architectures.
- The warmup schedule
- Set learning rate to 0 in the beginning.
- The learning rate increases linearly from 0 to initial learning rate during warmup period.
import math import torch from torch.optim import Optimizer from torch.optim.lr_scheduler import LambdaLR def get_cosine_schedule_with_warmup( optimizer: Optimizer, num_warmup_steps: int, num_training_steps: int, num_cycles: float = 0.5, last_epoch: int = -1, ): """ Create a schedule with a learning rate that decreases following the values of the cosine function between the initial lr set in the optimizer to 0, after a warmup period during which it increases linearly between 0 and the initial lr set in the optimizer. Args: optimizer (:class:`~torch.optim.Optimizer`): The optimizer for which to schedule the learning rate. num_warmup_steps (:obj:`int`): The number of steps for the warmup phase. num_training_steps (:obj:`int`): The total number of training steps. num_cycles (:obj:`float`, `optional`, defaults to 0.5): The number of waves in the cosine schedule (the defaults is to just decrease from the max value to 0 following a half-cosine). last_epoch (:obj:`int`, `optional`, defaults to -1): The index of the last epoch when resuming training. Return: :obj:`torch.optim.lr_scheduler.LambdaLR` with the appropriate schedule. """ def lr_lambda(current_step): # Warmup if current_step < num_warmup_steps: return float(current_step) / float(max(1, num_warmup_steps)) # decadence progress = float(current_step - num_warmup_steps) / float( max(1, num_training_steps - num_warmup_steps) ) return max( 0.0, 0.5 * (1.0 + math.cos(math.pi * float(num_cycles) * 2.0 * progress)) ) return LambdaLR(optimizer, lr_lambda, last_epoch)
Model Function
- Model forward function.
import torch def model_fn(batch, model, criterion, device): """Forward a batch through the model.""" mels, labels = batch mels = mels.to(device) labels = labels.to(device) outs = model(mels) loss = criterion(outs, labels) if AMS: loss = criterion(outs, labels) # Get the speaker id with highest probability. preds = outs.argmax(1) # Compute accuracy. accuracy = torch.mean((preds == labels).float()) return loss, accuracy
Validate
- Calculate accuracy of the validation set.
from tqdm.notebook import tqdm import torch def valid(dataloader, model, criterion, device): """Validate on validation set.""" model.eval() running_loss = 0.0 running_accuracy = 0.0 pbar = tqdm(total=len(dataloader.dataset), ncols=0, desc="Valid", unit=" uttr") for i, batch in enumerate(dataloader): with torch.no_grad(): loss, accuracy = model_fn(batch, model, criterion, device) running_loss += loss.item() running_accuracy += accuracy.item() pbar.update(dataloader.batch_size) pbar.set_postfix( loss=f"{running_loss / (i+1):.2f}", accuracy=f"{running_accuracy / (i+1):.2f}", ) pbar.close() model.train() return running_accuracy / len(dataloader)
Main function
from tqdm import tqdm import torch import torch.nn as nn from torch.optim import AdamW from torch.utils.data import DataLoader, random_split from torch.utils.tensorboard import SummaryWriter def parse_args(): """arguments""" config = { "data_dir": "./Dataset", "save_path": "model.ckpt", "batch_size": 32, "n_workers": 8, "valid_steps": 2000, "warmup_steps": 1000, "save_steps": 5000, 'pretrained_path':'model.ckpt', "total_steps": 70000, } return config def main( data_dir, save_path, batch_size, n_workers, valid_steps, warmup_steps, total_steps, save_steps, pretrained_path=None, ): """Main function.""" device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print(f"[Info]: Use {device} now!") train_loader, valid_loader, speaker_num = get_dataloader(data_dir, batch_size, n_workers) train_iterator = iter(train_loader) print(f"[Info]: Finish loading data!",flush = True) model = Classifier(n_spks=speaker_num).to(device) # 添加加载预训练模型的逻辑 if pretrained_path is not None: print(f"[Info]: Loading pretrained model from {pretrained_path}") model.load_state_dict(torch.load(pretrained_path)) criterion = nn.CrossEntropyLoss() if AMS == True: criterion = AMSoftmax().to(device) optimizer = AdamW(model.parameters(), lr=1e-3) scheduler = get_cosine_schedule_with_warmup(optimizer, warmup_steps, total_steps) print(f"[Info]: Finish creating model!",flush = True) best_accuracy = -1.0 best_state_dict = None writer = SummaryWriter() pbar = tqdm(total=valid_steps, ncols=0, desc="Train", unit=" step") for step in range(total_steps): # Get data try: batch = next(train_iterator) except StopIteration: train_iterator = iter(train_loader) batch = next(train_iterator) loss, accuracy = model_fn(batch, model, criterion, device) batch_loss = loss.item() batch_accuracy = accuracy.item() # Updata model loss.backward() optimizer.step() scheduler.step() optimizer.zero_grad() # Log pbar.update() pbar.set_postfix( loss=f"{batch_loss:.2f}", accuracy=f"{batch_accuracy:.2f}", step=step + 1, ) # Do validation if (step + 1) % valid_steps == 0: pbar.close() valid_accuracy = valid(valid_loader, model, criterion, device) # keep the best model if valid_accuracy > best_accuracy: best_accuracy = valid_accuracy best_state_dict = model.state_dict() pbar = tqdm(total=valid_steps, ncols=0, desc="Train", unit=" step") writer.add_scalar('Acc/train', batch_accuracy, step) writer.add_scalar('Acc/valid', valid_accuracy, step) writer.add_scalar('lr', optimizer.state_dict()['param_groups'][0]['lr'], step) # Save the best model so far. if (step + 1) % save_steps == 0 and best_state_dict is not None: torch.save(best_state_dict, save_path) pbar.write(f"Step {step + 1}, best model saved. (accuracy={best_accuracy:.4f})") pbar.close() return criterion # Decide whether to use AMSoftmax and Self-Attention Pooling global AMS, SAP AMS = False SAP = False # accuracy=0.8706 if __name__ == "__main__": criterion = main(**parse_args())
Inference
Dataset of inferenceoutput_lengths
import os import json import torch from pathlib import Path from torch.utils.data import Dataset class InferenceDataset(Dataset): def __init__(self, data_dir): testdata_path = Path(data_dir) / "testdata.json" metadata = json.load(testdata_path.open()) self.data_dir = data_dir self.data = metadata["utterances"] def __len__(self): return len(self.data) def __getitem__(self, index): utterance = self.data[index] feat_path = utterance["feature_path"] mel = torch.load(os.path.join(self.data_dir, feat_path)) return feat_path, mel def inference_collate_batch(batch): """Collate a batch of data.""" feat_paths, mels = zip(*batch) return feat_paths, torch.stack(mels)
Main function of Inference
import json import csv from pathlib import Path from tqdm.notebook import tqdm import torch from torch.utils.data import DataLoader def parse_args(): """arguments""" config = { "data_dir": "./Dataset", "model_path": "./model.ckpt", "output_path": "./output.csv", } return config def main( data_dir, model_path, output_path, ): """Main function.""" device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print(f"[Info]: Use {device} now!") mapping_path = Path(data_dir) / "mapping.json" mapping = json.load(mapping_path.open()) dataset = InferenceDataset(data_dir) dataloader = DataLoader( dataset, batch_size=1, shuffle=False, drop_last=False, num_workers=8, collate_fn=inference_collate_batch, ) print(f"[Info]: Finish loading data!",flush = True) speaker_num = len(mapping["id2speaker"]) model = Classifier(n_spks=speaker_num).to(device) model.load_state_dict(torch.load(model_path)) model.eval() print(f"[Info]: Finish creating model!",flush = True) results = [["Id", "Category"]] for feat_paths, mels in tqdm(dataloader): with torch.no_grad(): mels = mels.to(device) outs = model(mels) preds = outs.argmax(1).cpu().numpy() for feat_path, pred in zip(feat_paths, preds): results.append([feat_path, mapping["id2speaker"][str(pred)]]) with open(output_path, 'w', newline='') as csvfile: writer = csv.writer(csvfile) writer.writerows(results) if __name__ == "__main__": main(**parse_args())
评论加载中…