tiny-kws / model.py
priyadeepjaiswal9c's picture
demo app
682e4cc verified
Raw
History Blame Contribute Delete
2.93 kB
"""Depthwise-separable CNN for keyword spotting (DS-CNN).
Architecture follows the DS-CNN family from "Hello Edge: Keyword Spotting on
Microcontrollers" (Zhang et al., 2017, arXiv:1711.07128): one regular conv
stem, then a stack of depthwise-separable conv blocks, global average
pooling, and a single linear classifier.
A depthwise-separable conv factorizes a standard KxK conv into
(1) a depthwise KxK conv: one filter per channel, no cross-channel mixing
(2) a pointwise 1x1 conv: mixes channels, no spatial extent
which costs roughly C*(K*K + C) parameters instead of C*C*K*K — about 8-9x
fewer for K=3. That parameter efficiency is the whole point of the
"edge/TinyML" framing.
"""
import torch
import torch.nn as nn
class DSBlock(nn.Module):
def __init__(self, channels: int, stride: int = 1):
super().__init__()
self.depthwise = nn.Conv2d(
channels, channels, kernel_size=3, stride=stride, padding=1,
groups=channels, bias=False,
)
self.bn1 = nn.BatchNorm2d(channels)
self.pointwise = nn.Conv2d(channels, channels, kernel_size=1, bias=False)
self.bn2 = nn.BatchNorm2d(channels)
self.act = nn.ReLU(inplace=True)
def forward(self, x):
x = self.act(self.bn1(self.depthwise(x)))
x = self.act(self.bn2(self.pointwise(x)))
return x
class DSCNN(nn.Module):
"""Input: log-mel spectrogram (B, 1, 64, 101). Output: logits (B, 12)."""
def __init__(self, n_classes: int = 12, width: int = 160, n_blocks: int = 4,
dropout: float = 0.2):
super().__init__()
self.config = {"n_classes": n_classes, "width": width,
"n_blocks": n_blocks, "dropout": dropout}
# Stem: a tall-in-frequency kernel (10x4), stride 2 in both axes,
# as in the DS-CNN paper — quickly reduces the 64x101 input.
self.stem = nn.Sequential(
nn.Conv2d(1, width, kernel_size=(10, 4), stride=(2, 2),
padding=(5, 2), bias=False),
nn.BatchNorm2d(width),
nn.ReLU(inplace=True),
)
blocks = [DSBlock(width, stride=2)] # one more 2x downsample
blocks += [DSBlock(width) for _ in range(n_blocks - 1)]
self.blocks = nn.Sequential(*blocks)
self.pool = nn.AdaptiveAvgPool2d(1)
self.dropout = nn.Dropout(dropout)
self.fc = nn.Linear(width, n_classes)
def forward(self, x):
x = self.stem(x)
x = self.blocks(x)
x = self.pool(x).flatten(1)
return self.fc(self.dropout(x))
def count_parameters(model: nn.Module) -> int:
return sum(p.numel() for p in model.parameters() if p.requires_grad)
if __name__ == "__main__":
m = DSCNN()
x = torch.randn(2, 1, 64, 101)
y = m(x)
n = count_parameters(m)
print(f"output shape: {tuple(y.shape)}")
print(f"parameters: {n:,} ({n * 4 / 1e6:.2f} MB fp32)")