model_architectures.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680
  1. """
  2. Model architectures for Voice Recognition system.
  3. This module implements state-of-the-art speaker recognition model architectures:
  4. - ECAPA-TDNN: Enhanced version of Time Delay Neural Networks with attention
  5. - TitaNet-S: Small variant of the TitaNet architecture
  6. - SpeakerNet-M: Medium-sized SpeakerNet architecture
  7. All models are optimized for real-time inference and support dynamic speaker numbers.
  8. """
  9. import logging
  10. import math
  11. from typing import Optional, List, Dict, Any, Tuple, Union
  12. from enum import Enum
  13. import torch
  14. import torch.nn as nn
  15. import torch.nn.functional as F
  16. from torch.nn import Parameter
  17. # Configure logging
  18. logger = logging.getLogger(__name__)
  19. class ModelArchitectureType(Enum):
  20. """Supported model architecture types."""
  21. ECAPA_TDNN = "ecapa_tdnn"
  22. TITANET_S = "titanet_s"
  23. SPEAKERNET_M = "speakernet_m"
  24. class TDNNBlock(nn.Module):
  25. """Time Delay Neural Network block."""
  26. def __init__(self,
  27. in_channels: int,
  28. out_channels: int,
  29. kernel_size: int,
  30. dilation: int = 1,
  31. dropout: float = 0.1,
  32. use_batch_norm: bool = True):
  33. super().__init__()
  34. self.conv = nn.Conv1d(
  35. in_channels=in_channels,
  36. out_channels=out_channels,
  37. kernel_size=kernel_size,
  38. dilation=dilation,
  39. padding=(kernel_size - 1) * dilation // 2
  40. )
  41. self.use_batch_norm = use_batch_norm
  42. if use_batch_norm:
  43. self.batch_norm = nn.BatchNorm1d(out_channels)
  44. self.activation = nn.ReLU()
  45. self.dropout = nn.Dropout(dropout)
  46. def forward(self, x: torch.Tensor) -> torch.Tensor:
  47. """Forward pass through TDNN block."""
  48. x = self.conv(x)
  49. if self.use_batch_norm:
  50. x = self.batch_norm(x)
  51. x = self.activation(x)
  52. x = self.dropout(x)
  53. return x
  54. class SEBlock(nn.Module):
  55. """Squeeze-and-Excitation block for channel attention."""
  56. def __init__(self, channels: int, reduction: int = 16):
  57. super().__init__()
  58. self.global_pool = nn.AdaptiveAvgPool1d(1)
  59. self.fc = nn.Sequential(
  60. nn.Linear(channels, channels // reduction),
  61. nn.ReLU(inplace=True),
  62. nn.Linear(channels // reduction, channels),
  63. nn.Sigmoid()
  64. )
  65. def forward(self, x: torch.Tensor) -> torch.Tensor:
  66. """Forward pass through SE block."""
  67. batch_size, channels, _ = x.size()
  68. # Global average pooling
  69. y = self.global_pool(x).view(batch_size, channels)
  70. # Squeeze and excitation
  71. y = self.fc(y).view(batch_size, channels, 1)
  72. # Scale input
  73. return x * y
  74. class ResidualTDNNBlock(nn.Module):
  75. """Residual TDNN block with skip connections."""
  76. def __init__(self,
  77. channels: int,
  78. kernel_size: int,
  79. dilation: int = 1,
  80. dropout: float = 0.1,
  81. use_se: bool = True):
  82. super().__init__()
  83. self.tdnn1 = TDNNBlock(channels, channels, kernel_size, dilation, dropout)
  84. self.tdnn2 = TDNNBlock(channels, channels, kernel_size, dilation, dropout)
  85. self.use_se = use_se
  86. if use_se:
  87. self.se = SEBlock(channels)
  88. def forward(self, x: torch.Tensor) -> torch.Tensor:
  89. """Forward pass through residual TDNN block."""
  90. residual = x
  91. x = self.tdnn1(x)
  92. x = self.tdnn2(x)
  93. if self.use_se:
  94. x = self.se(x)
  95. return x + residual
  96. class AttentiveStatisticsPooling(nn.Module):
  97. """Attentive statistics pooling for temporal aggregation."""
  98. def __init__(self, input_dim: int):
  99. super().__init__()
  100. self.attention = nn.Linear(input_dim, 1)
  101. def forward(self, x: torch.Tensor) -> torch.Tensor:
  102. """
  103. Forward pass through attentive pooling.
  104. Args:
  105. x: Input tensor [batch_size, channels, time]
  106. Returns:
  107. Pooled tensor [batch_size, channels * 2]
  108. """
  109. # x: [batch_size, channels, time]
  110. batch_size, channels, time_steps = x.size()
  111. # Compute attention weights
  112. # Transpose to [batch_size, time, channels] for linear layer
  113. x_t = x.transpose(1, 2) # [batch_size, time, channels]
  114. # Compute attention scores
  115. alpha = self.attention(x_t) # [batch_size, time, 1]
  116. alpha = F.softmax(alpha, dim=1) # [batch_size, time, 1]
  117. # Apply attention to get weighted mean
  118. mean = torch.sum(alpha * x_t, dim=1) # [batch_size, channels]
  119. # Compute weighted standard deviation
  120. # First, compute weighted variance
  121. x_centered = x_t - mean.unsqueeze(1) # [batch_size, time, channels]
  122. var = torch.sum(alpha * (x_centered ** 2), dim=1) # [batch_size, channels]
  123. std = torch.sqrt(var + 1e-8) # [batch_size, channels]
  124. # Concatenate mean and std
  125. pooled = torch.cat([mean, std], dim=1) # [batch_size, channels * 2]
  126. return pooled
  127. class ECADATDNNModel(nn.Module):
  128. """
  129. ECAPA-TDNN (Emphasized Channel Attention, Propagation and Aggregation TDNN).
  130. Enhanced version of TDNN with:
  131. - Squeeze-and-Excitation blocks for channel attention
  132. - Residual connections
  133. - Attentive statistics pooling
  134. """
  135. def __init__(self,
  136. input_dim: int = 40,
  137. embedding_dim: int = 192,
  138. channels: int = 512,
  139. num_speakers: int = 4,
  140. dropout: float = 0.1,
  141. use_attention_pooling: bool = True):
  142. super().__init__()
  143. self.input_dim = input_dim
  144. self.embedding_dim = embedding_dim
  145. self.num_speakers = num_speakers
  146. # Frame-level processing
  147. self.frame_layer = TDNNBlock(input_dim, channels, kernel_size=5, dropout=dropout)
  148. # TDNN blocks with increasing dilation
  149. self.tdnn_blocks = nn.ModuleList([
  150. ResidualTDNNBlock(channels, kernel_size=3, dilation=1, dropout=dropout),
  151. ResidualTDNNBlock(channels, kernel_size=3, dilation=2, dropout=dropout),
  152. ResidualTDNNBlock(channels, kernel_size=3, dilation=3, dropout=dropout),
  153. ResidualTDNNBlock(channels, kernel_size=3, dilation=1, dropout=dropout),
  154. ])
  155. # Segment-level processing
  156. self.segment_layer = TDNNBlock(channels, channels * 3, kernel_size=1, dropout=dropout)
  157. # Pooling layer
  158. self.use_attention_pooling = use_attention_pooling
  159. if use_attention_pooling:
  160. self.pooling = AttentiveStatisticsPooling(channels * 3)
  161. pooling_output_dim = channels * 3 * 2 # mean + std
  162. else:
  163. self.pooling = None
  164. pooling_output_dim = channels * 3 * 2 # manual mean + std
  165. # Embedding layer
  166. self.embedding_layer = nn.Sequential(
  167. nn.Linear(pooling_output_dim, embedding_dim),
  168. nn.BatchNorm1d(embedding_dim),
  169. nn.ReLU(),
  170. nn.Dropout(dropout)
  171. )
  172. # Classification layer (for training)
  173. self.classifier = nn.Linear(embedding_dim, num_speakers)
  174. # Initialize weights
  175. self._initialize_weights()
  176. def _initialize_weights(self):
  177. """Initialize model weights."""
  178. for m in self.modules():
  179. if isinstance(m, nn.Conv1d):
  180. nn.init.xavier_normal_(m.weight)
  181. if m.bias is not None:
  182. nn.init.constant_(m.bias, 0)
  183. elif isinstance(m, nn.Linear):
  184. nn.init.xavier_normal_(m.weight)
  185. nn.init.constant_(m.bias, 0)
  186. elif isinstance(m, nn.BatchNorm1d):
  187. nn.init.constant_(m.weight, 1)
  188. nn.init.constant_(m.bias, 0)
  189. def forward(self, x: torch.Tensor, extract_embedding: bool = False) -> torch.Tensor:
  190. """
  191. Forward pass through ECAPA-TDNN.
  192. Args:
  193. x: Input features [batch_size, input_dim, time]
  194. extract_embedding: Whether to return embeddings instead of logits
  195. Returns:
  196. Either speaker embeddings or classification logits
  197. """
  198. # Frame-level processing
  199. x = self.frame_layer(x)
  200. # TDNN blocks
  201. for tdnn_block in self.tdnn_blocks:
  202. x = tdnn_block(x)
  203. # Segment-level processing
  204. x = self.segment_layer(x)
  205. # Temporal pooling
  206. if self.use_attention_pooling:
  207. x = self.pooling(x)
  208. else:
  209. # Manual statistics pooling
  210. mean = torch.mean(x, dim=2)
  211. std = torch.std(x, dim=2)
  212. x = torch.cat([mean, std], dim=1)
  213. # Embedding extraction
  214. embeddings = self.embedding_layer(x)
  215. if extract_embedding:
  216. return F.normalize(embeddings, p=2, dim=1)
  217. # Classification
  218. logits = self.classifier(embeddings)
  219. return logits
  220. def extract_embedding(self, x: torch.Tensor) -> torch.Tensor:
  221. """Extract speaker embeddings."""
  222. return self.forward(x, extract_embedding=True)
  223. class TitaNetSModel(nn.Module):
  224. """
  225. TitaNet-S (Small variant) architecture.
  226. Lightweight architecture optimized for real-time inference while
  227. maintaining good speaker recognition performance.
  228. """
  229. def __init__(self,
  230. input_dim: int = 40,
  231. embedding_dim: int = 192,
  232. channels: List[int] = None,
  233. num_speakers: int = 4,
  234. dropout: float = 0.05):
  235. super().__init__()
  236. if channels is None:
  237. channels = [128, 256, 512, 1024]
  238. self.input_dim = input_dim
  239. self.embedding_dim = embedding_dim
  240. self.num_speakers = num_speakers
  241. # Input projection
  242. self.input_conv = nn.Conv1d(input_dim, channels[0], kernel_size=1)
  243. self.input_bn = nn.BatchNorm1d(channels[0])
  244. # Encoder blocks
  245. self.encoder_blocks = nn.ModuleList()
  246. for i in range(len(channels) - 1):
  247. block = self._make_encoder_block(
  248. channels[i], channels[i + 1], dropout
  249. )
  250. self.encoder_blocks.append(block)
  251. # Global context
  252. self.global_context = nn.Sequential(
  253. nn.Conv1d(channels[-1], channels[-1], kernel_size=1),
  254. nn.BatchNorm1d(channels[-1]),
  255. nn.ReLU(),
  256. nn.Dropout(dropout)
  257. )
  258. # Pooling
  259. self.pooling = AttentiveStatisticsPooling(channels[-1])
  260. # Embedding layer
  261. self.embedding_layer = nn.Sequential(
  262. nn.Linear(channels[-1] * 2, embedding_dim),
  263. nn.BatchNorm1d(embedding_dim),
  264. nn.ReLU(),
  265. nn.Dropout(dropout)
  266. )
  267. # Classification layer
  268. self.classifier = nn.Linear(embedding_dim, num_speakers)
  269. self._initialize_weights()
  270. def _make_encoder_block(self, in_channels: int, out_channels: int, dropout: float):
  271. """Create encoder block."""
  272. return nn.Sequential(
  273. # Depthwise separable convolution
  274. nn.Conv1d(in_channels, in_channels, kernel_size=3, padding=1, groups=in_channels),
  275. nn.Conv1d(in_channels, out_channels, kernel_size=1),
  276. nn.BatchNorm1d(out_channels),
  277. nn.ReLU(),
  278. nn.Dropout(dropout),
  279. # Pointwise convolution
  280. nn.Conv1d(out_channels, out_channels, kernel_size=1),
  281. nn.BatchNorm1d(out_channels),
  282. nn.ReLU(),
  283. nn.Dropout(dropout)
  284. )
  285. def _initialize_weights(self):
  286. """Initialize model weights."""
  287. for m in self.modules():
  288. if isinstance(m, nn.Conv1d):
  289. nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
  290. if m.bias is not None:
  291. nn.init.constant_(m.bias, 0)
  292. elif isinstance(m, nn.Linear):
  293. nn.init.xavier_normal_(m.weight)
  294. nn.init.constant_(m.bias, 0)
  295. elif isinstance(m, nn.BatchNorm1d):
  296. nn.init.constant_(m.weight, 1)
  297. nn.init.constant_(m.bias, 0)
  298. def forward(self, x: torch.Tensor, extract_embedding: bool = False) -> torch.Tensor:
  299. """Forward pass through TitaNet-S."""
  300. # Input projection
  301. x = self.input_conv(x)
  302. x = self.input_bn(x)
  303. x = F.relu(x)
  304. # Encoder blocks
  305. for block in self.encoder_blocks:
  306. x = block(x)
  307. # Global context
  308. x = self.global_context(x)
  309. # Pooling
  310. x = self.pooling(x)
  311. # Embedding
  312. embeddings = self.embedding_layer(x)
  313. if extract_embedding:
  314. return F.normalize(embeddings, p=2, dim=1)
  315. # Classification
  316. logits = self.classifier(embeddings)
  317. return logits
  318. def extract_embedding(self, x: torch.Tensor) -> torch.Tensor:
  319. """Extract speaker embeddings."""
  320. return self.forward(x, extract_embedding=True)
  321. class SpeakerNetMModel(nn.Module):
  322. """
  323. SpeakerNet-M (Medium) architecture.
  324. LSTM-based architecture with attention mechanism for speaker recognition.
  325. Good balance between performance and computational efficiency.
  326. """
  327. def __init__(self,
  328. input_dim: int = 40,
  329. embedding_dim: int = 256,
  330. hidden_dim: int = 512,
  331. num_layers: int = 4,
  332. num_speakers: int = 4,
  333. dropout: float = 0.1,
  334. bidirectional: bool = True):
  335. super().__init__()
  336. self.input_dim = input_dim
  337. self.embedding_dim = embedding_dim
  338. self.hidden_dim = hidden_dim
  339. self.num_layers = num_layers
  340. self.num_speakers = num_speakers
  341. self.bidirectional = bidirectional
  342. # Input projection
  343. self.input_proj = nn.Linear(input_dim, hidden_dim)
  344. # LSTM layers
  345. self.lstm = nn.LSTM(
  346. input_size=hidden_dim,
  347. hidden_size=hidden_dim,
  348. num_layers=num_layers,
  349. dropout=dropout if num_layers > 1 else 0,
  350. bidirectional=bidirectional,
  351. batch_first=True
  352. )
  353. lstm_output_dim = hidden_dim * (2 if bidirectional else 1)
  354. # Attention mechanism
  355. self.attention = nn.Sequential(
  356. nn.Linear(lstm_output_dim, hidden_dim),
  357. nn.Tanh(),
  358. nn.Linear(hidden_dim, 1)
  359. )
  360. # Embedding layer
  361. self.embedding_layer = nn.Sequential(
  362. nn.Linear(lstm_output_dim, embedding_dim),
  363. nn.BatchNorm1d(embedding_dim),
  364. nn.ReLU(),
  365. nn.Dropout(dropout)
  366. )
  367. # Classification layer
  368. self.classifier = nn.Linear(embedding_dim, num_speakers)
  369. self._initialize_weights()
  370. def _initialize_weights(self):
  371. """Initialize model weights."""
  372. for name, param in self.named_parameters():
  373. if 'weight_ih' in name:
  374. nn.init.xavier_normal_(param.data)
  375. elif 'weight_hh' in name:
  376. nn.init.orthogonal_(param.data)
  377. elif 'bias' in name:
  378. nn.init.constant_(param.data, 0)
  379. for m in self.modules():
  380. if isinstance(m, nn.Linear):
  381. nn.init.xavier_normal_(m.weight)
  382. if m.bias is not None:
  383. nn.init.constant_(m.bias, 0)
  384. elif isinstance(m, nn.BatchNorm1d):
  385. nn.init.constant_(m.weight, 1)
  386. nn.init.constant_(m.bias, 0)
  387. def forward(self, x: torch.Tensor, extract_embedding: bool = False) -> torch.Tensor:
  388. """
  389. Forward pass through SpeakerNet-M.
  390. Args:
  391. x: Input features [batch_size, input_dim, time]
  392. extract_embedding: Whether to return embeddings
  393. Returns:
  394. Either embeddings or classification logits
  395. """
  396. # Transpose to [batch_size, time, input_dim] for LSTM
  397. x = x.transpose(1, 2)
  398. batch_size, seq_len, _ = x.size()
  399. # Input projection
  400. x = self.input_proj(x)
  401. x = F.relu(x)
  402. # LSTM processing
  403. lstm_out, _ = self.lstm(x) # [batch_size, seq_len, hidden_dim * directions]
  404. # Attention-based pooling
  405. attention_weights = self.attention(lstm_out) # [batch_size, seq_len, 1]
  406. attention_weights = F.softmax(attention_weights, dim=1)
  407. # Weighted sum
  408. weighted_output = torch.sum(attention_weights * lstm_out, dim=1) # [batch_size, hidden_dim * directions]
  409. # Embedding extraction
  410. embeddings = self.embedding_layer(weighted_output)
  411. if extract_embedding:
  412. return F.normalize(embeddings, p=2, dim=1)
  413. # Classification
  414. logits = self.classifier(embeddings)
  415. return logits
  416. def extract_embedding(self, x: torch.Tensor) -> torch.Tensor:
  417. """Extract speaker embeddings."""
  418. return self.forward(x, extract_embedding=True)
  419. def create_model_from_config(architecture_type: Union[str, ModelArchitectureType],
  420. input_dim: int,
  421. embedding_dim: int,
  422. num_speakers: int,
  423. **kwargs) -> nn.Module:
  424. """
  425. Factory function to create model from configuration.
  426. Args:
  427. architecture_type: Type of architecture
  428. input_dim: Input feature dimension
  429. embedding_dim: Output embedding dimension
  430. num_speakers: Number of speakers
  431. **kwargs: Additional architecture-specific parameters
  432. Returns:
  433. Configured model
  434. """
  435. if isinstance(architecture_type, str):
  436. architecture_type = ModelArchitectureType(architecture_type)
  437. if architecture_type == ModelArchitectureType.ECAPA_TDNN:
  438. return ECADATDNNModel(
  439. input_dim=input_dim,
  440. embedding_dim=embedding_dim,
  441. num_speakers=num_speakers,
  442. channels=kwargs.get('ecapa_channels', 512),
  443. dropout=kwargs.get('dropout_rate', 0.1),
  444. use_attention_pooling=kwargs.get('use_attention_pooling', True)
  445. )
  446. elif architecture_type == ModelArchitectureType.TITANET_S:
  447. return TitaNetSModel(
  448. input_dim=input_dim,
  449. embedding_dim=embedding_dim,
  450. num_speakers=num_speakers,
  451. channels=kwargs.get('titanet_channels', [128, 256, 512, 1024]),
  452. dropout=kwargs.get('dropout_rate', 0.05)
  453. )
  454. elif architecture_type == ModelArchitectureType.SPEAKERNET_M:
  455. return SpeakerNetMModel(
  456. input_dim=input_dim,
  457. embedding_dim=embedding_dim,
  458. num_speakers=num_speakers,
  459. hidden_dim=kwargs.get('speakernet_hidden_dim', 512),
  460. num_layers=kwargs.get('speakernet_num_layers', 4),
  461. dropout=kwargs.get('dropout_rate', 0.1)
  462. )
  463. else:
  464. raise ValueError(f"Unsupported architecture type: {architecture_type}")
  465. def get_supported_architectures() -> List[ModelArchitectureType]:
  466. """Get list of supported model architectures."""
  467. return list(ModelArchitectureType)
  468. def count_parameters(model: nn.Module) -> Dict[str, int]:
  469. """Count the number of parameters in a model."""
  470. trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
  471. total_params = sum(p.numel() for p in model.parameters())
  472. return {
  473. 'trainable_parameters': trainable_params,
  474. 'total_parameters': total_params,
  475. 'non_trainable_parameters': total_params - trainable_params
  476. }
  477. def estimate_model_size(model: nn.Module) -> Dict[str, float]:
  478. """Estimate model size and memory requirements."""
  479. param_count = count_parameters(model)
  480. # Estimate size in MB (assuming float32)
  481. param_size_mb = param_count['total_parameters'] * 4 / (1024 * 1024)
  482. # Estimate memory for training (rough approximation)
  483. # Parameters + gradients + optimizer states (Adam: 2x params) + activations
  484. training_memory_mb = param_size_mb * 4 # Conservative estimate
  485. # Estimate memory for inference
  486. inference_memory_mb = param_size_mb * 1.5 # Parameters + activations
  487. return {
  488. 'model_size_mb': param_size_mb,
  489. 'inference_memory_mb': inference_memory_mb,
  490. 'training_memory_mb': training_memory_mb
  491. }
  492. # Example usage and testing
  493. def test_model_architectures():
  494. """Test all model architectures with sample input."""
  495. batch_size = 4
  496. input_dim = 40
  497. seq_len = 100
  498. embedding_dim = 192
  499. num_speakers = 8
  500. # Create sample input
  501. x = torch.randn(batch_size, input_dim, seq_len)
  502. architectures = [
  503. (ModelArchitectureType.ECAPA_TDNN, {}),
  504. (ModelArchitectureType.TITANET_S, {}),
  505. (ModelArchitectureType.SPEAKERNET_M, {})
  506. ]
  507. for arch_type, kwargs in architectures:
  508. print(f"\nTesting {arch_type.value}:")
  509. # Create model
  510. model = create_model_from_config(
  511. arch_type, input_dim, embedding_dim, num_speakers, **kwargs
  512. )
  513. # Test forward pass
  514. model.eval()
  515. with torch.no_grad():
  516. # Classification output
  517. logits = model(x)
  518. print(f" Logits shape: {logits.shape}")
  519. # Embedding extraction
  520. embeddings = model.extract_embedding(x)
  521. print(f" Embeddings shape: {embeddings.shape}")
  522. # Model statistics
  523. param_stats = count_parameters(model)
  524. size_stats = estimate_model_size(model)
  525. print(f" Parameters: {param_stats['trainable_parameters']:,}")
  526. print(f" Model size: {size_stats['model_size_mb']:.2f} MB")
  527. print(f" Inference memory: {size_stats['inference_memory_mb']:.2f} MB")
  528. if __name__ == "__main__":
  529. test_model_architectures()