<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sameer Shah</title>
    <description>The latest articles on DEV Community by Sameer Shah (@sameershahh).</description>
    <link>https://dev.to/sameershahh</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3406145%2Fb5a20f51-d7f6-4ed7-abe7-099032522c24.png</url>
      <title>DEV Community: Sameer Shah</title>
      <link>https://dev.to/sameershahh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sameershahh"/>
    <language>en</language>
    <item>
      <title>How I Built a Facial-Expression Recognition Model with PyTorch (FER-2013, 72% Val Acc)</title>
      <dc:creator>Sameer Shah</dc:creator>
      <pubDate>Tue, 12 Aug 2025 21:30:24 +0000</pubDate>
      <link>https://dev.to/sameershahh/how-i-built-a-facial-expression-recognition-model-with-pytorch-fer-2013-72-val-acc-2oc3</link>
      <guid>https://dev.to/sameershahh/how-i-built-a-facial-expression-recognition-model-with-pytorch-fer-2013-72-val-acc-2oc3</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvkxm9qcjimu4uzao48y7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvkxm9qcjimu4uzao48y7.png" alt=" " width="586" height="258"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I trained a 3-block CNN in PyTorch on the FER-2013 dataset to classify 7 emotions. This post explains the dataset challenges, preprocessing and augmentation, exact model architecture, training recipe, evaluation (confusion matrix + per-class F1), and next steps for deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Emotion recognition enables richer human–computer interactions. I chose FER-2013 because it’s realistic: low-resolution (48×48), grayscale, and class-imbalanced. The goal: produce a reproducible, deployment-ready CNN pipeline that balances accuracy and efficiency for real-time inference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem statement
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Input: 48×48 grayscale faces.&lt;/li&gt;
&lt;li&gt;Task: 7-class classification — Angry, Disgust, Fear, Happy, Sad, Surprise, Neutral.&lt;/li&gt;
&lt;li&gt;Challenges: small images → limited features, class imbalance, noisy labels, and intra-class variation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Dataset &amp;amp; preprocessing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Source: FER-2013 (Kaggle). Split into train/val/test as in the original CSV (or your split).&lt;/li&gt;
&lt;li&gt;Preprocessing pipeline (PyTorch transforms):
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from torchvision import transforms

train_transform = transforms.Compose([
    transforms.ToPILImage(),
    transforms.RandomHorizontalFlip(),     # sensible for faces
    transforms.RandomRotation(10),         # small rotations
    transforms.RandomResizedCrop(48, scale=(0.9,1.0)),
    transforms.ToTensor(),
    transforms.Normalize((0.5,), (0.5,))
])

val_transform = transforms.Compose([
    transforms.ToPILImage(),
    transforms.Resize((48,48)),
    transforms.ToTensor(),
    transforms.Normalize((0.5,), (0.5,))
])

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Model architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fikuuv837jg3ojwncgddl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fikuuv837jg3ojwncgddl.png" alt=" " width="800" height="397"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Input: 48 × 48 × 1 (grayscale)&lt;/li&gt;
&lt;li&gt;Block 1: Conv2d(1 → 64, 3×3, pad=1) → BatchNorm2d(64) → ReLU → MaxPool2d(2×2) → OUTPUT 24×24×64&lt;/li&gt;
&lt;li&gt;Block 2: Conv2d(64 → 128, 3×3, pad=1) → BatchNorm2d(128) → ReLU → MaxPool2d(2×2) → OUTPUT 12×12×128&lt;/li&gt;
&lt;li&gt;Block 3: Conv2d(128 → 256, 3×3, pad=1) → BatchNorm2d(256) → ReLU → MaxPool2d(2×2) → OUTPUT 6×6×256&lt;/li&gt;
&lt;li&gt;Dropout2d(p=0.25) → Flatten (9216) → FC(9216 → 512) → ReLU → Dropout(p=0.5) → FC(512 → 7) → Softmax (inference)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Training recipe
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Loss: CrossEntropyLoss()&lt;/li&gt;
&lt;li&gt;Optimizer: AdamW(lr=1e-3, weight_decay=1e-4)&lt;/li&gt;
&lt;li&gt;Scheduler: ReduceLROnPlateau or CosineAnnealingLR (I used ReduceLROnPlateau on val loss)&lt;/li&gt;
&lt;li&gt;Batch size: 64 (adjust by GPU memory)&lt;/li&gt;
&lt;li&gt;Epochs: 30–60 with early stopping (patience 7 on val loss)&lt;/li&gt;
&lt;li&gt;Checkpoint: save best_model.pt by val F1 (or loss)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Minimal training loop snippet
&lt;/h2&gt;

&lt;p&gt;`criterion = nn.CrossEntropyLoss()&lt;br&gt;
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)&lt;br&gt;
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'min', patience=3)&lt;/p&gt;

&lt;p&gt;for epoch in range(1, epochs+1):&lt;br&gt;
    train_one_epoch(model, train_loader, optimizer, criterion)&lt;br&gt;
    val_loss, val_metrics = validate(model, val_loader, criterion)&lt;br&gt;
    scheduler.step(val_loss)&lt;br&gt;
    if val_metrics['f1_macro'] &amp;gt; best_f1:&lt;br&gt;
        best_f1 = val_metrics['f1_macro']&lt;br&gt;
        torch.save(model.state_dict(), 'best_model.pt')&lt;br&gt;
`&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducibility
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import random, numpy as np, torch
seed = 42
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Check code on &lt;a href="https://github.com/Sameershahh/Facial_Expression_Recognizer" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. If you want this adapted for a real-time webcam or a Django web deploy, contact me.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
