models.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717
  1. """
  2. State-of-the-art architectures for voice recognition.
  3. This module implements ECAPA-TDNN, TitaNet-S, and SpeakerNet-M architectures
  4. optimized for speaker recognition and voice identification tasks.
  5. """
  6. import math
  7. from typing import Dict, List, Optional, Tuple, Union
  8. import torch
  9. import torch.nn as nn
  10. import torch.nn.functional as F
  11. class SEBlock(nn.Module):
  12. """Squeeze-and-Excitation block for channel attention."""
  13. def __init__(self, channels: int, reduction: int = 16):
  14. super().__init__()
  15. self.squeeze = nn.AdaptiveAvgPool1d(1)
  16. self.excitation = nn.Sequential(
  17. nn.Linear(channels, channels // reduction, bias=False),
  18. nn.ReLU(inplace=True),
  19. nn.Linear(channels // reduction, channels, bias=False),
  20. nn.Sigmoid()
  21. )
  22. def forward(self, x):
  23. b, c, _ = x.size()
  24. y = self.squeeze(x).view(b, c)
  25. y = self.excitation(y).view(b, c, 1)
  26. return x * y.expand_as(x)
  27. class Res2Conv1d(nn.Module):
  28. """
  29. Res2Conv1d implementation for multi-scale feature extraction.
  30. Based on Res2Net architecture adapted for 1D convolutions.
  31. """
  32. def __init__(self, in_channels: int, out_channels: int, kernel_size: int = 3,
  33. dilation: int = 1, scale: int = 4):
  34. super().__init__()
  35. assert out_channels % scale == 0, "out_channels must be divisible by scale"
  36. self.scale = scale
  37. self.out_channels = out_channels
  38. width = out_channels // scale
  39. self.convs = nn.ModuleList()
  40. for i in range(scale - 1):
  41. self.convs.append(
  42. nn.Conv1d(width, width, kernel_size, padding=kernel_size//2 * dilation,
  43. dilation=dilation, bias=False)
  44. )
  45. self.bn = nn.BatchNorm1d(out_channels)
  46. self.relu = nn.ReLU(inplace=True)
  47. def forward(self, x):
  48. batch_size = x.size(0)
  49. # Split input into scale parts
  50. spx = torch.split(x, self.out_channels // self.scale, dim=1)
  51. outputs = [spx[0]]
  52. for i in range(1, self.scale):
  53. if i == 1:
  54. sp = spx[i]
  55. else:
  56. sp = spx[i] + outputs[-1]
  57. sp = self.convs[i-1](sp)
  58. outputs.append(sp)
  59. out = torch.cat(outputs, dim=1)
  60. out = self.bn(out)
  61. out = self.relu(out)
  62. return out
  63. class TDNN(nn.Module):
  64. """Time Delay Neural Network layer."""
  65. def __init__(self, in_channels: int, out_channels: int, kernel_size: int = 5,
  66. dilation: int = 1, use_bn: bool = True, use_relu: bool = True):
  67. super().__init__()
  68. self.conv = nn.Conv1d(
  69. in_channels, out_channels, kernel_size,
  70. padding=kernel_size//2 * dilation, dilation=dilation, bias=not use_bn
  71. )
  72. self.bn = nn.BatchNorm1d(out_channels) if use_bn else None
  73. self.relu = nn.ReLU(inplace=True) if use_relu else None
  74. def forward(self, x):
  75. x = self.conv(x)
  76. if self.bn is not None:
  77. x = self.bn(x)
  78. if self.relu is not None:
  79. x = self.relu(x)
  80. return x
  81. class StatisticalPooling(nn.Module):
  82. """Statistical pooling layer that computes mean and standard deviation."""
  83. def __init__(self, input_dim: int, output_dim: Optional[int] = None):
  84. super().__init__()
  85. self.input_dim = input_dim
  86. self.output_dim = output_dim or input_dim * 2
  87. if output_dim and output_dim != input_dim * 2:
  88. self.projection = nn.Linear(input_dim * 2, output_dim)
  89. else:
  90. self.projection = None
  91. def forward(self, x):
  92. # x shape: (batch, channels, time)
  93. mean = torch.mean(x, dim=2)
  94. std = torch.std(x, dim=2)
  95. # Concatenate mean and std
  96. stats = torch.cat([mean, std], dim=1)
  97. if self.projection is not None:
  98. stats = self.projection(stats)
  99. return stats
  100. class AttentiveStatisticalPooling(nn.Module):
  101. """Attentive statistical pooling with learnable attention weights."""
  102. def __init__(self, input_dim: int, attention_dim: int = 128):
  103. super().__init__()
  104. self.input_dim = input_dim
  105. self.attention_dim = attention_dim
  106. self.attention = nn.Sequential(
  107. nn.Linear(input_dim, attention_dim),
  108. nn.Tanh(),
  109. nn.Linear(attention_dim, 1)
  110. )
  111. def forward(self, x):
  112. # x shape: (batch, channels, time)
  113. batch_size, channels, time_steps = x.shape
  114. # Transpose for attention computation
  115. x_t = x.transpose(1, 2) # (batch, time, channels)
  116. # Compute attention weights
  117. attention_weights = self.attention(x_t) # (batch, time, 1)
  118. attention_weights = F.softmax(attention_weights, dim=1)
  119. # Apply attention weights
  120. weighted_x = x_t * attention_weights # (batch, time, channels)
  121. # Compute weighted statistics
  122. mean = torch.sum(weighted_x, dim=1) # (batch, channels)
  123. # Compute weighted standard deviation
  124. diff = x_t - mean.unsqueeze(1)
  125. weighted_var = torch.sum(attention_weights * diff ** 2, dim=1)
  126. std = torch.sqrt(weighted_var + 1e-8)
  127. # Concatenate mean and std
  128. stats = torch.cat([mean, std], dim=1)
  129. return stats
  130. class ECAPA_TDNN(nn.Module):
  131. """
  132. ECAPA-TDNN: Emphasized Channel Attention, Propagation and Aggregation
  133. in TDNN based Speaker Verification.
  134. Reference: "ECAPA-TDNN: Emphasized Channel Attention, Propagation and
  135. Aggregation in TDNN based Speaker Verification" by Desplanques et al.
  136. """
  137. def __init__(self, input_dim: int = 40, channels: int = 512,
  138. embedding_dim: int = 192, num_speakers: int = None,
  139. use_attention_pooling: bool = True):
  140. """
  141. Initialize ECAPA-TDNN model.
  142. Args:
  143. input_dim: Input feature dimension (e.g., 40 for mel spectrograms)
  144. channels: Number of channels in TDNN layers
  145. embedding_dim: Dimension of speaker embeddings
  146. num_speakers: Number of speakers for classification (None for embeddings only)
  147. use_attention_pooling: Whether to use attentive statistical pooling
  148. """
  149. super().__init__()
  150. self.input_dim = input_dim
  151. self.channels = channels
  152. self.embedding_dim = embedding_dim
  153. self.num_speakers = num_speakers
  154. # Frame-level feature extraction
  155. self.frame_conv = nn.Conv1d(input_dim, channels, 5, padding=2)
  156. # ECAPA blocks
  157. self.ecapa1 = self._make_ecapa_block(channels, channels, kernel_size=3, dilation=2)
  158. self.ecapa2 = self._make_ecapa_block(channels, channels, kernel_size=3, dilation=3)
  159. self.ecapa3 = self._make_ecapa_block(channels, channels, kernel_size=3, dilation=4)
  160. self.ecapa4 = self._make_ecapa_block(channels, 3 * channels, kernel_size=1, dilation=1)
  161. # Aggregation
  162. self.aggregation_conv = nn.Conv1d(3 * channels, 1536, 1)
  163. # Pooling
  164. if use_attention_pooling:
  165. self.pooling = AttentiveStatisticalPooling(1536)
  166. pooling_output_dim = 1536 * 2
  167. else:
  168. self.pooling = StatisticalPooling(1536)
  169. pooling_output_dim = 1536 * 2
  170. # Batch normalization after pooling
  171. self.bn_pooling = nn.BatchNorm1d(pooling_output_dim)
  172. # Embedding layer
  173. self.embedding = nn.Linear(pooling_output_dim, embedding_dim)
  174. self.bn_embedding = nn.BatchNorm1d(embedding_dim)
  175. # Classification layer (optional)
  176. if num_speakers is not None:
  177. self.classifier = nn.Linear(embedding_dim, num_speakers)
  178. else:
  179. self.classifier = None
  180. self._initialize_weights()
  181. def _make_ecapa_block(self, in_channels: int, out_channels: int,
  182. kernel_size: int = 3, dilation: int = 1):
  183. """Create an ECAPA block with Res2Conv and SE."""
  184. return nn.Sequential(
  185. Res2Conv1d(in_channels, out_channels, kernel_size, dilation, scale=8),
  186. SEBlock(out_channels, reduction=16)
  187. )
  188. def forward(self, x, return_embedding: bool = False):
  189. """
  190. Forward pass through ECAPA-TDNN.
  191. Args:
  192. x: Input tensor (batch, features, time)
  193. return_embedding: Whether to return embeddings instead of classification
  194. Returns:
  195. If num_speakers is None or return_embedding is True: speaker embeddings
  196. Otherwise: classification logits
  197. """
  198. # Frame-level processing
  199. x = self.frame_conv(x)
  200. # ECAPA blocks with residual connections
  201. x1 = self.ecapa1(x)
  202. x = x + x1
  203. x2 = self.ecapa2(x)
  204. x = x + x2
  205. x3 = self.ecapa3(x)
  206. x = x + x3
  207. # Aggregation (concatenate all frame-level features)
  208. x_agg = torch.cat([x1, x2, x3], dim=1)
  209. x = self.ecapa4(x_agg)
  210. x = self.aggregation_conv(x)
  211. # Statistical pooling
  212. x = self.pooling(x)
  213. x = self.bn_pooling(x)
  214. # Embedding
  215. embedding = self.embedding(x)
  216. embedding = self.bn_embedding(embedding)
  217. if self.classifier is None or return_embedding:
  218. return F.normalize(embedding, p=2, dim=1)
  219. else:
  220. logits = self.classifier(embedding)
  221. return logits
  222. def get_embeddings(self, x):
  223. """Get speaker embeddings."""
  224. return self.forward(x, return_embedding=True)
  225. def _initialize_weights(self):
  226. """Initialize model weights."""
  227. for m in self.modules():
  228. if isinstance(m, nn.Conv1d):
  229. nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
  230. if m.bias is not None:
  231. nn.init.constant_(m.bias, 0)
  232. elif isinstance(m, nn.BatchNorm1d):
  233. nn.init.constant_(m.weight, 1)
  234. nn.init.constant_(m.bias, 0)
  235. elif isinstance(m, nn.Linear):
  236. nn.init.normal_(m.weight, 0, 0.01)
  237. nn.init.constant_(m.bias, 0)
  238. class TitaNet_S(nn.Module):
  239. """
  240. TitaNet-S: A lightweight variant of TitaNet for speaker recognition.
  241. Based on the TitaNet architecture but with reduced parameters
  242. for efficient speaker recognition.
  243. """
  244. def __init__(self, input_dim: int = 40, channels: List[int] = None,
  245. embedding_dim: int = 192, num_speakers: int = None,
  246. dropout_rate: float = 0.1):
  247. """
  248. Initialize TitaNet-S model.
  249. Args:
  250. input_dim: Input feature dimension
  251. channels: List of channel dimensions for each block
  252. embedding_dim: Dimension of speaker embeddings
  253. num_speakers: Number of speakers for classification
  254. dropout_rate: Dropout rate for regularization
  255. """
  256. super().__init__()
  257. if channels is None:
  258. channels = [128, 256, 512, 1024]
  259. self.input_dim = input_dim
  260. self.channels = channels
  261. self.embedding_dim = embedding_dim
  262. self.num_speakers = num_speakers
  263. # Input projection
  264. self.input_conv = nn.Conv1d(input_dim, channels[0], 1)
  265. self.input_bn = nn.BatchNorm1d(channels[0])
  266. # TitaNet blocks
  267. self.blocks = nn.ModuleList()
  268. for i in range(len(channels) - 1):
  269. block = self._make_titanet_block(
  270. channels[i], channels[i+1], dropout_rate
  271. )
  272. self.blocks.append(block)
  273. # Statistical pooling
  274. self.pooling = AttentiveStatisticalPooling(channels[-1])
  275. # Embedding layers
  276. pooling_dim = channels[-1] * 2
  277. self.embedding_layers = nn.Sequential(
  278. nn.Linear(pooling_dim, channels[-1]),
  279. nn.BatchNorm1d(channels[-1]),
  280. nn.ReLU(inplace=True),
  281. nn.Dropout(dropout_rate),
  282. nn.Linear(channels[-1], embedding_dim),
  283. nn.BatchNorm1d(embedding_dim)
  284. )
  285. # Classification layer (optional)
  286. if num_speakers is not None:
  287. self.classifier = nn.Linear(embedding_dim, num_speakers)
  288. else:
  289. self.classifier = None
  290. self._initialize_weights()
  291. def _make_titanet_block(self, in_channels: int, out_channels: int,
  292. dropout_rate: float):
  293. """Create a TitaNet block."""
  294. return nn.Sequential(
  295. # Depthwise separable convolution
  296. nn.Conv1d(in_channels, in_channels, 3, padding=1, groups=in_channels),
  297. nn.Conv1d(in_channels, out_channels, 1),
  298. nn.BatchNorm1d(out_channels),
  299. nn.ReLU(inplace=True),
  300. nn.Dropout1d(dropout_rate),
  301. # Squeeze-and-Excitation
  302. SEBlock(out_channels, reduction=8),
  303. # Residual connection (if dimensions match)
  304. # Note: Actual residual connection would need dimension matching
  305. )
  306. def forward(self, x, return_embedding: bool = False):
  307. """Forward pass through TitaNet-S."""
  308. # Input projection
  309. x = self.input_conv(x)
  310. x = self.input_bn(x)
  311. x = F.relu(x, inplace=True)
  312. # TitaNet blocks
  313. for block in self.blocks:
  314. x = block(x)
  315. # Statistical pooling
  316. x = self.pooling(x)
  317. # Embedding
  318. embedding = self.embedding_layers(x)
  319. if self.classifier is None or return_embedding:
  320. return F.normalize(embedding, p=2, dim=1)
  321. else:
  322. logits = self.classifier(embedding)
  323. return logits
  324. def get_embeddings(self, x):
  325. """Get speaker embeddings."""
  326. return self.forward(x, return_embedding=True)
  327. def _initialize_weights(self):
  328. """Initialize model weights."""
  329. for m in self.modules():
  330. if isinstance(m, nn.Conv1d):
  331. nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
  332. if m.bias is not None:
  333. nn.init.constant_(m.bias, 0)
  334. elif isinstance(m, nn.BatchNorm1d):
  335. nn.init.constant_(m.weight, 1)
  336. nn.init.constant_(m.bias, 0)
  337. elif isinstance(m, nn.Linear):
  338. nn.init.normal_(m.weight, 0, 0.01)
  339. nn.init.constant_(m.bias, 0)
  340. class SpeakerNet_M(nn.Module):
  341. """
  342. SpeakerNet-M: Medium-sized speaker recognition network.
  343. A balanced architecture for speaker recognition that provides
  344. good performance with moderate computational requirements.
  345. """
  346. def __init__(self, input_dim: int = 40, hidden_dim: int = 512,
  347. embedding_dim: int = 256, num_speakers: int = None,
  348. num_layers: int = 4, dropout_rate: float = 0.1):
  349. """
  350. Initialize SpeakerNet-M model.
  351. Args:
  352. input_dim: Input feature dimension
  353. hidden_dim: Hidden dimension for TDNN layers
  354. embedding_dim: Dimension of speaker embeddings
  355. num_speakers: Number of speakers for classification
  356. num_layers: Number of TDNN layers
  357. dropout_rate: Dropout rate for regularization
  358. """
  359. super().__init__()
  360. self.input_dim = input_dim
  361. self.hidden_dim = hidden_dim
  362. self.embedding_dim = embedding_dim
  363. self.num_speakers = num_speakers
  364. self.num_layers = num_layers
  365. # Input layer
  366. self.input_layer = TDNN(input_dim, hidden_dim, kernel_size=5)
  367. # TDNN layers with different dilations
  368. self.tdnn_layers = nn.ModuleList()
  369. dilations = [1, 2, 3, 4, 5][:num_layers]
  370. for i, dilation in enumerate(dilations):
  371. layer = TDNN(
  372. hidden_dim, hidden_dim,
  373. kernel_size=3, dilation=dilation
  374. )
  375. self.tdnn_layers.append(layer)
  376. # Context expansion
  377. self.context_conv = nn.Conv1d(hidden_dim, hidden_dim * 2, 1)
  378. self.context_bn = nn.BatchNorm1d(hidden_dim * 2)
  379. # Statistical pooling
  380. self.pooling = StatisticalPooling(hidden_dim * 2)
  381. # Embedding layers
  382. pooling_dim = hidden_dim * 4 # 2x for mean+std
  383. self.embedding_layers = nn.Sequential(
  384. nn.Linear(pooling_dim, hidden_dim),
  385. nn.BatchNorm1d(hidden_dim),
  386. nn.ReLU(inplace=True),
  387. nn.Dropout(dropout_rate),
  388. nn.Linear(hidden_dim, embedding_dim),
  389. nn.BatchNorm1d(embedding_dim)
  390. )
  391. # Classification layer (optional)
  392. if num_speakers is not None:
  393. self.classifier = nn.Sequential(
  394. nn.Dropout(dropout_rate),
  395. nn.Linear(embedding_dim, num_speakers)
  396. )
  397. else:
  398. self.classifier = None
  399. self._initialize_weights()
  400. def forward(self, x, return_embedding: bool = False):
  401. """Forward pass through SpeakerNet-M."""
  402. # Input processing
  403. x = self.input_layer(x)
  404. # TDNN layers with residual connections
  405. for layer in self.tdnn_layers:
  406. residual = x
  407. x = layer(x)
  408. x = x + residual # Residual connection
  409. # Context expansion
  410. x = self.context_conv(x)
  411. x = self.context_bn(x)
  412. x = F.relu(x, inplace=True)
  413. # Statistical pooling
  414. x = self.pooling(x)
  415. # Embedding
  416. embedding = self.embedding_layers(x)
  417. if self.classifier is None or return_embedding:
  418. return F.normalize(embedding, p=2, dim=1)
  419. else:
  420. logits = self.classifier(embedding)
  421. return logits
  422. def get_embeddings(self, x):
  423. """Get speaker embeddings."""
  424. return self.forward(x, return_embedding=True)
  425. def _initialize_weights(self):
  426. """Initialize model weights."""
  427. for m in self.modules():
  428. if isinstance(m, nn.Conv1d):
  429. nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
  430. if m.bias is not None:
  431. nn.init.constant_(m.bias, 0)
  432. elif isinstance(m, nn.BatchNorm1d):
  433. nn.init.constant_(m.weight, 1)
  434. nn.init.constant_(m.bias, 0)
  435. elif isinstance(m, nn.Linear):
  436. nn.init.normal_(m.weight, 0, 0.01)
  437. nn.init.constant_(m.bias, 0)
  438. def create_voice_recognition_model(model_type: str = "ecapa_tdnn", **kwargs) -> nn.Module:
  439. """
  440. Factory function to create voice recognition models.
  441. Args:
  442. model_type: Type of model ('ecapa_tdnn', 'titanet_s', 'speakernet_m')
  443. **kwargs: Additional arguments for model initialization
  444. Returns:
  445. Voice recognition model instance
  446. """
  447. if model_type.lower() == "ecapa_tdnn":
  448. return ECAPA_TDNN(**kwargs)
  449. elif model_type.lower() == "titanet_s":
  450. return TitaNet_S(**kwargs)
  451. elif model_type.lower() == "speakernet_m":
  452. return SpeakerNet_M(**kwargs)
  453. else:
  454. raise ValueError(f"Unknown model type: {model_type}")
  455. def count_parameters(model: nn.Module) -> Dict[str, int]:
  456. """Count model parameters."""
  457. total_params = sum(p.numel() for p in model.parameters())
  458. trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
  459. return {
  460. "total_parameters": total_params,
  461. "trainable_parameters": trainable_params,
  462. "non_trainable_parameters": total_params - trainable_params
  463. }
  464. class AngularMarginLoss(nn.Module):
  465. """
  466. Angular Margin Loss (ArcFace) for speaker recognition.
  467. Reference: "ArcFace: Additive Angular Margin Loss for Deep Face Recognition"
  468. """
  469. def __init__(self, embedding_dim: int, num_speakers: int,
  470. margin: float = 0.5, scale: float = 64.0):
  471. """
  472. Initialize Angular Margin Loss.
  473. Args:
  474. embedding_dim: Dimension of input embeddings
  475. num_speakers: Number of speaker classes
  476. margin: Angular margin (m)
  477. scale: Scale factor (s)
  478. """
  479. super().__init__()
  480. self.embedding_dim = embedding_dim
  481. self.num_speakers = num_speakers
  482. self.margin = margin
  483. self.scale = scale
  484. # Weight matrix (represents speaker centroids)
  485. self.weight = nn.Parameter(torch.randn(num_speakers, embedding_dim))
  486. nn.init.xavier_uniform_(self.weight)
  487. self.cos_margin = math.cos(margin)
  488. self.sin_margin = math.sin(margin)
  489. self.threshold = math.cos(math.pi - margin)
  490. self.mm = math.sin(math.pi - margin) * margin
  491. def forward(self, embeddings: torch.Tensor, labels: torch.Tensor) -> torch.Tensor:
  492. """
  493. Compute Angular Margin Loss.
  494. Args:
  495. embeddings: Speaker embeddings (batch_size, embedding_dim)
  496. labels: Speaker labels (batch_size,)
  497. Returns:
  498. Loss value
  499. """
  500. # Normalize embeddings and weights
  501. embeddings = F.normalize(embeddings, p=2, dim=1)
  502. weight = F.normalize(self.weight, p=2, dim=1)
  503. # Compute cosine similarity
  504. cosine = F.linear(embeddings, weight) # (batch_size, num_speakers)
  505. sine = torch.sqrt(1.0 - torch.pow(cosine, 2))
  506. # Compute phi (cosine with margin)
  507. phi = cosine * self.cos_margin - sine * self.sin_margin
  508. # Apply threshold
  509. phi = torch.where(cosine > self.threshold, phi, cosine - self.mm)
  510. # Create one-hot labels
  511. one_hot = torch.zeros_like(cosine)
  512. one_hot.scatter_(1, labels.view(-1, 1).long(), 1)
  513. # Apply margin to target class
  514. output = (one_hot * phi) + ((1.0 - one_hot) * cosine)
  515. output *= self.scale
  516. return F.cross_entropy(output, labels)
  517. class GE2ELoss(nn.Module):
  518. """
  519. Generalized End-to-End Loss for speaker verification.
  520. Reference: "Generalized End-to-End Loss for Speaker Verification"
  521. """
  522. def __init__(self, init_w: float = 10.0, init_b: float = -5.0):
  523. """
  524. Initialize GE2E Loss.
  525. Args:
  526. init_w: Initial value for learnable weight
  527. init_b: Initial value for learnable bias
  528. """
  529. super().__init__()
  530. self.w = nn.Parameter(torch.tensor(init_w))
  531. self.b = nn.Parameter(torch.tensor(init_b))
  532. def forward(self, embeddings: torch.Tensor, labels: torch.Tensor) -> torch.Tensor:
  533. """
  534. Compute GE2E Loss.
  535. Args:
  536. embeddings: Speaker embeddings (batch_size, embedding_dim)
  537. labels: Speaker labels (batch_size,)
  538. Returns:
  539. Loss value
  540. """
  541. # Normalize embeddings
  542. embeddings = F.normalize(embeddings, p=2, dim=1)
  543. # Compute centroids for each speaker
  544. unique_labels = torch.unique(labels)
  545. centroids = []
  546. for label in unique_labels:
  547. mask = labels == label
  548. centroid = torch.mean(embeddings[mask], dim=0)
  549. centroids.append(centroid)
  550. centroids = torch.stack(centroids)
  551. centroids = F.normalize(centroids, p=2, dim=1)
  552. # Compute similarities
  553. similarities = torch.mm(embeddings, centroids.t())
  554. # Apply learnable parameters
  555. similarities = torch.abs(self.w) * similarities + self.b
  556. # Create target labels (map original labels to centroid indices)
  557. target_labels = torch.zeros_like(labels)
  558. for i, label in enumerate(unique_labels):
  559. mask = labels == label
  560. target_labels[mask] = i
  561. return F.cross_entropy(similarities, target_labels)