models.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. """
  2. RepCNN and related architectures for wakeword detection.
  3. This module implements the RepCNN architecture and variants optimized for
  4. wakeword detection tasks with different computational requirements.
  5. """
  6. import math
  7. from typing import Dict, List, Optional, Tuple
  8. import torch
  9. import torch.nn as nn
  10. import torch.nn.functional as F
  11. class DepthwiseSeparableConv2d(nn.Module):
  12. """Depthwise separable convolution for efficient processing."""
  13. def __init__(self, in_channels: int, out_channels: int,
  14. kernel_size: int, stride: int = 1, padding: int = 0):
  15. super().__init__()
  16. self.depthwise = nn.Conv2d(
  17. in_channels, in_channels, kernel_size, stride, padding,
  18. groups=in_channels, bias=False
  19. )
  20. self.pointwise = nn.Conv2d(in_channels, out_channels, 1, bias=False)
  21. def forward(self, x):
  22. x = self.depthwise(x)
  23. x = self.pointwise(x)
  24. return x
  25. class SEBlock(nn.Module):
  26. """Squeeze-and-Excitation block for attention mechanism."""
  27. def __init__(self, channels: int, reduction: int = 16):
  28. super().__init__()
  29. self.squeeze = nn.AdaptiveAvgPool2d(1)
  30. self.excitation = nn.Sequential(
  31. nn.Linear(channels, channels // reduction, bias=False),
  32. nn.ReLU(inplace=True),
  33. nn.Linear(channels // reduction, channels, bias=False),
  34. nn.Sigmoid()
  35. )
  36. def forward(self, x):
  37. b, c, _, _ = x.size()
  38. y = self.squeeze(x).view(b, c)
  39. y = self.excitation(y).view(b, c, 1, 1)
  40. return x * y.expand_as(x)
  41. class RepConvBlock(nn.Module):
  42. """
  43. Reparameterizable convolution block.
  44. During training, uses multiple paths that can be reparameterized
  45. into a single convolution during inference for efficiency.
  46. """
  47. def __init__(self, in_channels: int, out_channels: int,
  48. kernel_size: int = 3, stride: int = 1, padding: int = 1,
  49. groups: int = 1, use_se: bool = False):
  50. super().__init__()
  51. self.in_channels = in_channels
  52. self.out_channels = out_channels
  53. self.kernel_size = kernel_size
  54. self.stride = stride
  55. self.padding = padding
  56. self.groups = groups
  57. self.use_se = use_se
  58. # Main 3x3 convolution
  59. self.conv_3x3 = nn.Conv2d(
  60. in_channels, out_channels, kernel_size, stride, padding,
  61. groups=groups, bias=False
  62. )
  63. # 1x1 convolution branch
  64. self.conv_1x1 = nn.Conv2d(
  65. in_channels, out_channels, 1, stride, 0, groups=groups, bias=False
  66. ) if kernel_size > 1 else None
  67. # Identity branch (only if input and output channels match)
  68. self.identity = (
  69. nn.BatchNorm2d(out_channels) if in_channels == out_channels and stride == 1
  70. else None
  71. )
  72. # Batch normalization
  73. self.bn = nn.BatchNorm2d(out_channels)
  74. # SE block
  75. self.se = SEBlock(out_channels) if use_se else None
  76. # Activation
  77. self.activation = nn.ReLU(inplace=True)
  78. # Flag to track if reparameterized
  79. self.is_reparameterized = False
  80. def forward(self, x):
  81. if self.is_reparameterized:
  82. # Use single reparameterized convolution
  83. out = self.reparam_conv(x)
  84. out = self.bn(out)
  85. else:
  86. # Use multiple branches during training
  87. out = self.conv_3x3(x)
  88. if self.conv_1x1 is not None:
  89. out += self.conv_1x1(x)
  90. if self.identity is not None:
  91. out += x
  92. out = self.bn(out)
  93. if self.se is not None:
  94. out = self.se(out)
  95. out = self.activation(out)
  96. return out
  97. def reparameterize(self):
  98. """Reparameterize multiple branches into single convolution."""
  99. if self.is_reparameterized:
  100. return
  101. # Get weights from different branches
  102. w_3x3 = self.conv_3x3.weight
  103. # Initialize combined weight
  104. w_combined = w_3x3.clone()
  105. # Add 1x1 convolution weight (pad to 3x3)
  106. if self.conv_1x1 is not None:
  107. w_1x1 = self.conv_1x1.weight
  108. pad_size = (self.kernel_size - 1) // 2
  109. w_1x1_padded = F.pad(w_1x1, [pad_size] * 4)
  110. w_combined += w_1x1_padded
  111. # Add identity weight
  112. if self.identity is not None:
  113. identity_weight = torch.zeros_like(w_3x3)
  114. center = self.kernel_size // 2
  115. for i in range(self.in_channels):
  116. identity_weight[i, i, center, center] = 1.0
  117. w_combined += identity_weight
  118. # Create reparameterized convolution
  119. self.reparam_conv = nn.Conv2d(
  120. self.in_channels, self.out_channels, self.kernel_size,
  121. self.stride, self.padding, groups=self.groups, bias=False
  122. )
  123. self.reparam_conv.weight.data = w_combined
  124. # Remove original branches
  125. self.__delattr__('conv_3x3')
  126. if hasattr(self, 'conv_1x1'):
  127. self.__delattr__('conv_1x1')
  128. if hasattr(self, 'identity'):
  129. self.__delattr__('identity')
  130. self.is_reparameterized = True
  131. class RepCNN(nn.Module):
  132. """
  133. RepCNN architecture for wakeword detection.
  134. Based on RepVGG but optimized for audio spectrograms and wakeword detection.
  135. Supports reparameterization for efficient inference.
  136. """
  137. def __init__(self, num_classes: int = 3, input_channels: int = 1,
  138. num_mels: int = 40, time_frames: int = 151,
  139. width_multiplier: float = 1.0, use_se: bool = False,
  140. dropout_rate: float = 0.2):
  141. """
  142. Initialize RepCNN model.
  143. Args:
  144. num_classes: Number of output classes (e.g., 3 for custom/system/negative)
  145. input_channels: Number of input channels (1 for mono spectrograms)
  146. num_mels: Number of mel filterbank features
  147. time_frames: Number of time frames in spectrogram
  148. width_multiplier: Width multiplier for channels
  149. use_se: Whether to use Squeeze-and-Excitation blocks
  150. dropout_rate: Dropout rate for regularization
  151. """
  152. super().__init__()
  153. self.num_classes = num_classes
  154. self.input_channels = input_channels
  155. self.num_mels = num_mels
  156. self.time_frames = time_frames
  157. self.use_se = use_se
  158. # Calculate channel dimensions
  159. def make_divisible(v, divisor=8):
  160. return max(divisor, int(v + divisor / 2) // divisor * divisor)
  161. channels = [
  162. make_divisible(64 * width_multiplier),
  163. make_divisible(128 * width_multiplier),
  164. make_divisible(256 * width_multiplier),
  165. make_divisible(512 * width_multiplier)
  166. ]
  167. # Initial convolution layer
  168. self.conv1 = RepConvBlock(
  169. input_channels, channels[0], kernel_size=3, stride=1, padding=1, use_se=use_se
  170. )
  171. # Stage 1: Process frequency information
  172. self.stage1 = nn.Sequential(
  173. RepConvBlock(channels[0], channels[0], use_se=use_se),
  174. RepConvBlock(channels[0], channels[1], stride=(2, 1), use_se=use_se), # Downsample freq only
  175. )
  176. # Stage 2: Process both frequency and time
  177. self.stage2 = nn.Sequential(
  178. RepConvBlock(channels[1], channels[1], use_se=use_se),
  179. RepConvBlock(channels[1], channels[2], stride=2, use_se=use_se), # Downsample both
  180. )
  181. # Stage 3: Final feature extraction
  182. self.stage3 = nn.Sequential(
  183. RepConvBlock(channels[2], channels[2], use_se=use_se),
  184. RepConvBlock(channels[2], channels[3], stride=2, use_se=use_se),
  185. )
  186. # Global pooling and classification
  187. self.global_pool = nn.AdaptiveAvgPool2d(1)
  188. self.dropout = nn.Dropout(dropout_rate)
  189. # Calculate the expected feature size after convolutions
  190. self._calculate_feature_size()
  191. self.classifier = nn.Linear(channels[3], num_classes)
  192. # Initialize weights
  193. self._initialize_weights()
  194. def _calculate_feature_size(self):
  195. """Calculate feature map size after convolutions."""
  196. # This is mainly for verification - we use adaptive pooling
  197. with torch.no_grad():
  198. dummy_input = torch.zeros(1, self.input_channels, self.num_mels, self.time_frames)
  199. x = self.conv1(dummy_input)
  200. x = self.stage1(x)
  201. x = self.stage2(x)
  202. x = self.stage3(x)
  203. self.final_feature_size = x.shape[1:]
  204. def forward(self, x):
  205. """Forward pass through RepCNN."""
  206. # Input shape: (batch, channels, mel_bins, time_frames)
  207. x = self.conv1(x)
  208. x = self.stage1(x)
  209. x = self.stage2(x)
  210. x = self.stage3(x)
  211. # Global average pooling
  212. x = self.global_pool(x)
  213. x = x.flatten(1)
  214. # Classification
  215. x = self.dropout(x)
  216. x = self.classifier(x)
  217. return x
  218. def get_embeddings(self, x):
  219. """Get feature embeddings before classification."""
  220. x = self.conv1(x)
  221. x = self.stage1(x)
  222. x = self.stage2(x)
  223. x = self.stage3(x)
  224. x = self.global_pool(x)
  225. x = x.flatten(1)
  226. return x
  227. def reparameterize(self):
  228. """Reparameterize all RepConv blocks for inference."""
  229. self.conv1.reparameterize()
  230. for module in self.stage1:
  231. if isinstance(module, RepConvBlock):
  232. module.reparameterize()
  233. for module in self.stage2:
  234. if isinstance(module, RepConvBlock):
  235. module.reparameterize()
  236. for module in self.stage3:
  237. if isinstance(module, RepConvBlock):
  238. module.reparameterize()
  239. def _initialize_weights(self):
  240. """Initialize model weights."""
  241. for m in self.modules():
  242. if isinstance(m, nn.Conv2d):
  243. nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
  244. if m.bias is not None:
  245. nn.init.constant_(m.bias, 0)
  246. elif isinstance(m, nn.BatchNorm2d):
  247. nn.init.constant_(m.weight, 1)
  248. nn.init.constant_(m.bias, 0)
  249. elif isinstance(m, nn.Linear):
  250. nn.init.normal_(m.weight, 0, 0.01)
  251. nn.init.constant_(m.bias, 0)
  252. class ImprovedRepCNN(RepCNN):
  253. """
  254. Improved RepCNN with additional optimizations for wakeword detection.
  255. Includes temporal attention, improved skip connections, and better
  256. handling of the time dimension in spectrograms.
  257. """
  258. def __init__(self, num_classes: int = 3, input_channels: int = 1,
  259. num_mels: int = 40, time_frames: int = 151,
  260. width_multiplier: float = 1.0, use_se: bool = True,
  261. dropout_rate: float = 0.2, use_temporal_attention: bool = True):
  262. """Initialize Improved RepCNN."""
  263. super().__init__(
  264. num_classes, input_channels, num_mels, time_frames,
  265. width_multiplier, use_se, dropout_rate
  266. )
  267. self.use_temporal_attention = use_temporal_attention
  268. if use_temporal_attention:
  269. # Add temporal attention mechanism
  270. channels = [
  271. int(64 * width_multiplier),
  272. int(128 * width_multiplier),
  273. int(256 * width_multiplier),
  274. int(512 * width_multiplier)
  275. ]
  276. self.temporal_attention = nn.Sequential(
  277. nn.Conv1d(channels[3], channels[3] // 4, 1),
  278. nn.ReLU(inplace=True),
  279. nn.Conv1d(channels[3] // 4, channels[3], 1),
  280. nn.Sigmoid()
  281. )
  282. def forward(self, x):
  283. """Forward pass with temporal attention."""
  284. # Input shape: (batch, channels, mel_bins, time_frames)
  285. x = self.conv1(x)
  286. x = self.stage1(x)
  287. x = self.stage2(x)
  288. x = self.stage3(x)
  289. if self.use_temporal_attention:
  290. # Apply temporal attention
  291. b, c, h, w = x.shape
  292. # Global average pool over frequency dimension
  293. temporal_features = F.adaptive_avg_pool2d(x, (1, w)).squeeze(2) # (b, c, w)
  294. # Apply temporal attention
  295. attention_weights = self.temporal_attention(temporal_features) # (b, c, w)
  296. # Apply attention to original features
  297. attention_weights = attention_weights.unsqueeze(2) # (b, c, 1, w)
  298. x = x * attention_weights
  299. # Global average pooling
  300. x = self.global_pool(x)
  301. x = x.flatten(1)
  302. # Classification
  303. x = self.dropout(x)
  304. x = self.classifier(x)
  305. return x
  306. class LightweightRepCNN(nn.Module):
  307. """
  308. Lightweight RepCNN optimized for edge devices and real-time inference.
  309. Uses depthwise separable convolutions, reduced channels, and optimized
  310. architecture for minimal computational requirements.
  311. """
  312. def __init__(self, num_classes: int = 3, input_channels: int = 1,
  313. num_mels: int = 40, time_frames: int = 151,
  314. width_multiplier: float = 0.5, dropout_rate: float = 0.1):
  315. """Initialize Lightweight RepCNN."""
  316. super().__init__()
  317. self.num_classes = num_classes
  318. self.input_channels = input_channels
  319. self.num_mels = num_mels
  320. self.time_frames = time_frames
  321. # Reduced channel dimensions for efficiency
  322. def make_divisible(v, divisor=8):
  323. return max(divisor, int(v + divisor / 2) // divisor * divisor)
  324. channels = [
  325. make_divisible(32 * width_multiplier),
  326. make_divisible(64 * width_multiplier),
  327. make_divisible(128 * width_multiplier),
  328. make_divisible(256 * width_multiplier)
  329. ]
  330. # Initial regular convolution
  331. self.conv1 = nn.Sequential(
  332. nn.Conv2d(input_channels, channels[0], 3, 1, 1, bias=False),
  333. nn.BatchNorm2d(channels[0]),
  334. nn.ReLU(inplace=True)
  335. )
  336. # Depthwise separable convolution blocks
  337. self.stage1 = nn.Sequential(
  338. self._make_depthwise_block(channels[0], channels[0]),
  339. self._make_depthwise_block(channels[0], channels[1], stride=(2, 1))
  340. )
  341. self.stage2 = nn.Sequential(
  342. self._make_depthwise_block(channels[1], channels[1]),
  343. self._make_depthwise_block(channels[1], channels[2], stride=2)
  344. )
  345. self.stage3 = nn.Sequential(
  346. self._make_depthwise_block(channels[2], channels[2]),
  347. self._make_depthwise_block(channels[2], channels[3], stride=2)
  348. )
  349. # Efficient global pooling and classification
  350. self.global_pool = nn.AdaptiveAvgPool2d(1)
  351. self.dropout = nn.Dropout(dropout_rate)
  352. self.classifier = nn.Linear(channels[3], num_classes)
  353. self._initialize_weights()
  354. def _make_depthwise_block(self, in_channels: int, out_channels: int,
  355. stride: Tuple[int, int] = (1, 1)):
  356. """Create depthwise separable convolution block."""
  357. return nn.Sequential(
  358. # Depthwise convolution
  359. nn.Conv2d(in_channels, in_channels, 3, stride, 1,
  360. groups=in_channels, bias=False),
  361. nn.BatchNorm2d(in_channels),
  362. nn.ReLU(inplace=True),
  363. # Pointwise convolution
  364. nn.Conv2d(in_channels, out_channels, 1, 1, 0, bias=False),
  365. nn.BatchNorm2d(out_channels),
  366. nn.ReLU(inplace=True)
  367. )
  368. def forward(self, x):
  369. """Forward pass through Lightweight RepCNN."""
  370. x = self.conv1(x)
  371. x = self.stage1(x)
  372. x = self.stage2(x)
  373. x = self.stage3(x)
  374. x = self.global_pool(x)
  375. x = x.flatten(1)
  376. x = self.dropout(x)
  377. x = self.classifier(x)
  378. return x
  379. def get_embeddings(self, x):
  380. """Get feature embeddings before classification."""
  381. x = self.conv1(x)
  382. x = self.stage1(x)
  383. x = self.stage2(x)
  384. x = self.stage3(x)
  385. x = self.global_pool(x)
  386. x = x.flatten(1)
  387. return x
  388. def _initialize_weights(self):
  389. """Initialize model weights."""
  390. for m in self.modules():
  391. if isinstance(m, nn.Conv2d):
  392. nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
  393. if m.bias is not None:
  394. nn.init.constant_(m.bias, 0)
  395. elif isinstance(m, nn.BatchNorm2d):
  396. nn.init.constant_(m.weight, 1)
  397. nn.init.constant_(m.bias, 0)
  398. elif isinstance(m, nn.Linear):
  399. nn.init.normal_(m.weight, 0, 0.01)
  400. nn.init.constant_(m.bias, 0)
  401. def create_repcnn_model(model_type: str = "standard", **kwargs) -> nn.Module:
  402. """
  403. Factory function to create RepCNN models.
  404. Args:
  405. model_type: Type of RepCNN model ('standard', 'improved', 'lightweight')
  406. **kwargs: Additional arguments for model initialization
  407. Returns:
  408. RepCNN model instance
  409. """
  410. if model_type == "standard":
  411. return RepCNN(**kwargs)
  412. elif model_type == "improved":
  413. return ImprovedRepCNN(**kwargs)
  414. elif model_type == "lightweight":
  415. return LightweightRepCNN(**kwargs)
  416. else:
  417. raise ValueError(f"Unknown model type: {model_type}")
  418. def count_parameters(model: nn.Module) -> Dict[str, int]:
  419. """Count model parameters."""
  420. total_params = sum(p.numel() for p in model.parameters())
  421. trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
  422. return {
  423. "total_parameters": total_params,
  424. "trainable_parameters": trainable_params,
  425. "non_trainable_parameters": total_params - trainable_params
  426. }
  427. def estimate_model_flops(model: nn.Module, input_shape: Tuple[int, ...]) -> int:
  428. """
  429. Estimate FLOPs for model inference (simplified calculation).
  430. Args:
  431. model: PyTorch model
  432. input_shape: Input tensor shape (C, H, W)
  433. Returns:
  434. Estimated FLOPs
  435. """
  436. def conv_flops(in_channels, out_channels, kernel_size, output_size):
  437. return in_channels * out_channels * kernel_size * kernel_size * output_size[0] * output_size[1]
  438. def linear_flops(in_features, out_features):
  439. return in_features * out_features
  440. # This is a simplified estimation - for accurate FLOPs counting,
  441. # use specialized libraries like thop or fvcore
  442. flops = 0
  443. # Approximate based on model type
  444. if isinstance(model, (RepCNN, ImprovedRepCNN)):
  445. # Rough estimation for RepCNN
  446. flops = input_shape[1] * input_shape[2] * 1000000 # ~1M FLOPs per pixel
  447. elif isinstance(model, LightweightRepCNN):
  448. # Lower FLOPs for lightweight model
  449. flops = input_shape[1] * input_shape[2] * 500000 # ~0.5M FLOPs per pixel
  450. return flops