namsel_BUDA_CNN / model.py
trabten's picture
Initial model upload
7e6c7fa verified
Raw
History Blame Contribute Delete
5.08 kB
"""CNN model for Tibetan character recognition.
CPU-optimized architecture based on deep learning best practices:
- Depthwise separable convolutions (~8-18x fewer operations)
- Batch normalization (faster convergence, higher learning rates)
- Residual connections (deeper networks without vanishing gradients)
- Global Average Pooling (eliminates millions of FC parameters)
- He initialization for ReLU networks
Input: 32x32 grayscale character images (1 channel)
Output: num_classes logits
"""
import torch.nn as nn
class DepthwiseSeparableConv(nn.Module):
"""Depthwise separable convolution: depthwise + pointwise.
Factored convolution that reduces computation by ~8-18x compared
to standard convolution for 3x3 kernels.
"""
def __init__(self, in_ch, out_ch, kernel_size=3, stride=1, padding=1):
super().__init__()
self.depthwise = nn.Conv2d(
in_ch, in_ch, kernel_size,
stride=stride, padding=padding, groups=in_ch, bias=False
)
self.pointwise = nn.Conv2d(in_ch, out_ch, 1, bias=False)
self.bn = nn.BatchNorm2d(out_ch)
self.relu = nn.ReLU(inplace=True)
def forward(self, x):
x = self.depthwise(x)
x = self.pointwise(x)
x = self.bn(x)
x = self.relu(x)
return x
class ResidualBlock(nn.Module):
"""Residual block with two depthwise separable convolutions.
Skip connection adds input directly to output, enabling
training of deeper networks without vanishing gradients.
"""
def __init__(self, channels):
super().__init__()
self.conv1 = DepthwiseSeparableConv(channels, channels)
# Second conv without final ReLU (applied after residual add)
self.conv2_dw = nn.Conv2d(
channels, channels, 3, padding=1, groups=channels, bias=False
)
self.conv2_pw = nn.Conv2d(channels, channels, 1, bias=False)
self.bn2 = nn.BatchNorm2d(channels)
self.relu = nn.ReLU(inplace=True)
def forward(self, x):
residual = x
out = self.conv1(x)
out = self.conv2_dw(out)
out = self.conv2_pw(out)
out = self.bn2(out)
out = out + residual
out = self.relu(out)
return out
class TibetanCNN(nn.Module):
"""CPU-optimized CNN for Tibetan character classification.
Architecture:
Input (1x32x32)
-> Conv2d(32, 3x3) + BN + ReLU [32x32x32] stem
-> DSConv(32->64, stride=2) [64x16x16] downsample
-> ResidualBlock(64) [64x16x16]
-> DSConv(64->128, stride=2) [128x8x8] downsample
-> ResidualBlock(128) [128x8x8]
-> DSConv(128->256, stride=2) [256x4x4] downsample
-> Global Average Pooling [256]
-> Dropout
-> FC(256, num_classes)
~350K parameters. Inference: <5ms per character on CPU.
"""
def __init__(self, num_classes, dropout=0.3):
super().__init__()
# Stem: standard conv to learn from raw pixels
self.stem = nn.Sequential(
nn.Conv2d(1, 32, 3, padding=1, bias=False),
nn.BatchNorm2d(32),
nn.ReLU(inplace=True),
)
# Downsample 32x32 -> 16x16, expand 32 -> 64
self.down1 = DepthwiseSeparableConv(32, 64, stride=2)
self.res1 = ResidualBlock(64)
# Downsample 16x16 -> 8x8, expand 64 -> 128
self.down2 = DepthwiseSeparableConv(64, 128, stride=2)
self.res2 = ResidualBlock(128)
# Downsample 8x8 -> 4x4, expand 128 -> 256
self.down3 = DepthwiseSeparableConv(128, 256, stride=2)
# Classifier
self.gap = nn.AdaptiveAvgPool2d(1)
self.dropout = nn.Dropout(dropout)
self.fc = nn.Linear(256, num_classes)
self._init_weights()
def _init_weights(self):
"""He initialization for ReLU networks."""
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear):
nn.init.kaiming_normal_(m.weight)
nn.init.constant_(m.bias, 0)
def forward(self, x):
x = self.stem(x) # [B, 32, 32, 32]
x = self.down1(x) # [B, 64, 16, 16]
x = self.res1(x) # [B, 64, 16, 16]
x = self.down2(x) # [B, 128, 8, 8]
x = self.res2(x) # [B, 128, 8, 8]
x = self.down3(x) # [B, 256, 4, 4]
x = self.gap(x) # [B, 256, 1, 1]
x = x.view(x.size(0), -1) # [B, 256]
x = self.dropout(x)
x = self.fc(x) # [B, num_classes]
return x