ear-stt / model_lib.py
CDT5058's picture
Add model card, code, and artifacts
7df09bd verified
Raw History Blame Contribute Delete
5.02 kB
"""TinyConformer-CTC model, matching the notebook-03 architecture exactly.
Presets:
nano (96, 5): ~1.4M params, ~1.4MB int8
micro (112, 6): ~2.1M params, ~2.1MB int8
tiny (128, 6): ~2.8M params, ~2.8MB int8
small (256, 9): ~16M params, ~16MB int8 ← v2 target
CTC (not RNN-T/seq2seq) so the decode stays trivial. QKV is explicit Linear layers
(not nn.MultiheadAttention) so int8 dynamic quantization covers every big matmul.
Pre-LN on every sublayer (ff1, attn, conv, ff2) for training stability at larger sizes.
"""
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
class FFN(nn.Module):
def __init__(self, d, mult=4, p=0.1):
super().__init__()
self.net = nn.Sequential(nn.Linear(d, mult*d), nn.SiLU(), nn.Dropout(p),
nn.Linear(mult*d, d), nn.Dropout(p))
def forward(self, x): return self.net(x)
class MHSA(nn.Module):
"""Self-attention with explicit Linear QKV/O so int8 dynamic quant covers it."""
def __init__(self, d, heads=4, p=0.1):
super().__init__()
assert d % heads == 0
self.h, self.dh = heads, d // heads
self.qkv = nn.Linear(d, 3*d)
self.o = nn.Linear(d, d)
self.drop = nn.Dropout(p)
def forward(self, x, pad_mask=None):
B, T, D = x.shape
q, k, v = self.qkv(x).chunk(3, -1)
q = q.view(B, T, self.h, self.dh).transpose(1, 2)
k = k.view(B, T, self.h, self.dh).transpose(1, 2)
v = v.view(B, T, self.h, self.dh).transpose(1, 2)
s = q @ k.transpose(-2, -1) / math.sqrt(self.dh)
if pad_mask is not None:
s = s.masked_fill(pad_mask[:, None, None, :], float("-inf"))
w = self.drop(s.softmax(-1))
return self.o((w @ v).transpose(1, 2).reshape(B, T, D))
class ConvModule(nn.Module):
def __init__(self, d, k=15, p=0.1):
super().__init__()
self.pw1 = nn.Conv1d(d, 2*d, 1)
self.dw = nn.Conv1d(d, d, k, padding=k//2, groups=d)
self.bn = nn.BatchNorm1d(d)
self.pw2 = nn.Conv1d(d, d, 1)
self.drop = nn.Dropout(p)
def forward(self, x): # (B,T,D)
x = x.transpose(1, 2)
x = self.pw1(x)
a, b = x.chunk(2, 1)
x = a * torch.sigmoid(b)
x = self.bn(self.dw(x))
x = self.drop(self.pw2(F.silu(x)))
return x.transpose(1, 2)
class ConformerBlock(nn.Module):
def __init__(self, d=112, heads=4, p=0.1):
super().__init__()
self.ff1_norm = nn.LayerNorm(d)
self.ff1 = FFN(d, p=p)
self.attn_norm = nn.LayerNorm(d)
self.attn = MHSA(d, heads, p)
self.conv_norm = nn.LayerNorm(d)
self.conv = ConvModule(d, p=p)
self.ff2_norm = nn.LayerNorm(d)
self.ff2 = FFN(d, p=p)
self.final = nn.LayerNorm(d)
def forward(self, x, pad_mask=None):
x = x + 0.5 * self.ff1(self.ff1_norm(x))
x = x + self.attn(self.attn_norm(x), pad_mask)
x = x + self.conv(self.conv_norm(x))
x = x + 0.5 * self.ff2(self.ff2_norm(x))
return self.final(x)
class TinyConformerCTC(nn.Module):
N_MELS = 80
def __init__(self, d=112, n_blocks=6, n_classes=29, p=0.1):
super().__init__()
self.c1 = nn.Conv2d(1, d, 3, stride=2, padding=1)
self.c2 = nn.Conv2d(d, d, 3, stride=2, padding=1)
self.proj = nn.Linear(d * (self.N_MELS // 4), d)
self.blocks = nn.ModuleList([ConformerBlock(d, p=p) for _ in range(n_blocks)])
self.head = nn.Linear(d, n_classes)
@staticmethod
def out_len(l): # two stride-2 k3p1 convs
return ((l + 2 - 3)//2 + 1 + 2 - 3)//2 + 1
def forward(self, mel, lens):
x = F.relu(self.c1(mel)); x = F.relu(self.c2(x)) # (B,d,T/4,20)
B, C, T, F_ = x.shape
x = self.proj(x.permute(0, 2, 1, 3).reshape(B, T, C*F_))
# tensor arithmetic keeps out_len ONNX-traceable (avoids baking a constant)
ol = (lens.long().to(x.device) - 1) // 2 + 1
ol = (ol - 1) // 2 + 1
pad = torch.arange(T, device=x.device)[None, :] >= ol[:, None]
for blk in self.blocks:
x = blk(x, pad)
return self.head(x).log_softmax(-1), ol
def count_params(m):
n = sum(p.numel() for p in m.parameters())
print(f"params: {n/1e6:.2f}M | fp32 {n*4/1e6:.1f}MB | int8 ~{n/1e6:.1f}MB (linears)")
return n
def greedy_decode(log_probs, alphabet, blank=0):
"""log_probs: (T, C) -> (text, confidence). Collapses repeats, drops blanks."""
probs = log_probs.exp()
ids = probs.argmax(-1).tolist()
conf = probs.amax(-1)
out, prev, cs = [], None, []
for t, i in enumerate(ids):
if i != blank:
cs.append(float(conf[t]))
if i != prev:
out.append(alphabet[i])
prev = i
text = "".join(out)
return text, (sum(cs)/len(cs) if cs else 0.0)
# confidence < threshold -> "uncertain" (tongue's tie-reporting analog)
UNCERTAIN_TH = 0.35