<?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: Sheikh Sadi Asif</title>
    <description>The latest articles on DEV Community by Sheikh Sadi Asif (@gradienninja).</description>
    <link>https://dev.to/gradienninja</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%2F3907216%2F5263a8bb-e87c-4dc9-a0d0-d47fbc9e0acb.jpeg</url>
      <title>DEV Community: Sheikh Sadi Asif</title>
      <link>https://dev.to/gradienninja</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gradienninja"/>
    <language>en</language>
    <item>
      <title>I Built an AI That Detects Pneumonia From Chest X-Rays Here's Exactly How I Did It</title>
      <dc:creator>Sheikh Sadi Asif</dc:creator>
      <pubDate>Fri, 01 May 2026 08:36:02 +0000</pubDate>
      <link>https://dev.to/gradienninja/i-built-an-ai-that-detects-pneumonia-from-chest-x-rays-heres-exactly-how-i-did-it-52cp</link>
      <guid>https://dev.to/gradienninja/i-built-an-ai-that-detects-pneumonia-from-chest-x-rays-heres-exactly-how-i-did-it-52cp</guid>
      <description>&lt;p&gt;A few weeks ago, I shipped &lt;strong&gt;PneumoScan AI&lt;/strong&gt;  a deep learning model that analyzes chest X-ray images and detects pneumonia in seconds, with 90%+ accuracy. It's live, it's free, and anyone can use it right now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔗 &lt;a href="https://pneumonia-scan-ai.netlify.app/" rel="noopener noreferrer"&gt;pneumonia-scan-ai.netlify.app&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the story of how I built it — and everything I learned along the way.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Pneumonia?
&lt;/h2&gt;

&lt;p&gt;Pneumonia kills over 2 million people annually. A huge portion of those deaths happen in low-resource areas where radiologists are scarce and diagnosis is slow.&lt;/p&gt;

&lt;p&gt;I'm not claiming to solve that problem. But I wanted to build something that &lt;em&gt;mattered&lt;/em&gt;  not just another MNIST classifier or iris flower predictor. Medical imaging felt real.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Dataset
&lt;/h2&gt;

&lt;p&gt;I used the &lt;strong&gt;&lt;a href="https://www.kaggle.com/datasets/paultimothymooney/chest-xray-pneumonia" rel="noopener noreferrer"&gt;Chest X-Ray Images (Pneumonia)&lt;/a&gt;&lt;/strong&gt; dataset from Kaggle.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;5,800+ clinical chest X-ray images&lt;/li&gt;
&lt;li&gt;Two classes: &lt;code&gt;NORMAL&lt;/code&gt; and &lt;code&gt;PNEUMONIA&lt;/code&gt; (Viral &amp;amp; Bacterial)&lt;/li&gt;
&lt;li&gt;Real hospital data from Guangzhou Women and Children's Medical Center&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing I learned immediately: &lt;strong&gt;the dataset is imbalanced&lt;/strong&gt;. There are significantly more pneumonia images than normal ones. This is something you have to think about carefully in medical AI  because a model that just predicts "pneumonia" on everything could still hit decent accuracy numbers while being completely useless.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture — MobileNetV2 + Custom Head
&lt;/h2&gt;

&lt;p&gt;I chose &lt;strong&gt;MobileNetV2&lt;/strong&gt; as my base model for two reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It's lightweight (14MB)  perfect for deployment on free-tier infrastructure&lt;/li&gt;
&lt;li&gt;It was pre-trained on ImageNet, so it already knows how to extract visual features&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key idea is &lt;strong&gt;transfer learning&lt;/strong&gt;  instead of training a CNN from scratch on 5,800 images (which isn't enough), I used MobileNetV2 as a frozen feature extractor and added my own classification head on top.&lt;/p&gt;

&lt;p&gt;Here's the full pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input (any size RGB image)
    ↓
Resizing Layer      → 224 × 224 (built into model)
Rescaling Layer     → pixel values ÷ 255
    ↓
MobileNetV2         → FROZEN (trainable = False)
16 inverted residual blocks
Final feature map: 7 × 7 × 1280
    ↓
GlobalAveragePooling2D  → collapses to 1280 values
    ↓
Dense(128, ReLU)        → learns pneumonia-specific patterns
    ↓
Dropout(0.3)            → prevents overfitting
    ↓
Dense(1, Sigmoid)       → outputs probability (0 = Normal, 1 = Pneumonia)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why freeze MobileNetV2?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because it already knows how to see. The ImageNet weights encode knowledge about edges, textures, and shapes that transfer surprisingly well to X-rays. Fine-tuning all those layers on a small dataset would just cause overfitting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why add Dense(128) before the output?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The raw GlobalAveragePooling output is 1280 features  most of which are irrelevant to pneumonia. The Dense(128) layer acts as a bottleneck, forcing the model to compress what it learned into the most useful 128 features for this specific task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Training config:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Optimizer: Adam (lr=0.001)&lt;/li&gt;
&lt;li&gt;Loss: Binary Crossentropy&lt;/li&gt;
&lt;li&gt;Final accuracy: &lt;strong&gt;90%+&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Problem I Didn't Expect The Dog Test
&lt;/h2&gt;

&lt;p&gt;After I deployed the first version, I decided to test it.&lt;/p&gt;

&lt;p&gt;I uploaded a photo of a dog sitting in a bathroom smoking a cigarette.&lt;/p&gt;

&lt;p&gt;The result?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PNEUMONIA DETECTED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Confidence Score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"100.00%"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;100% confident. That dog had pneumonia.&lt;/p&gt;

&lt;p&gt;This is a classic failure mode of CNNs  &lt;strong&gt;the model has no concept of "this isn't even an X-ray."&lt;/strong&gt; It was trained only on X-rays, so it forced every single image into one of two buckets regardless of what it actually was.&lt;/p&gt;

&lt;p&gt;I needed an input validation layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix  Saturation Gate
&lt;/h2&gt;

&lt;p&gt;The solution I came up with is what I call a &lt;strong&gt;Saturation Gate&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The logic is simple: chest X-rays are grayscale. Any real X-ray will have near-zero color saturation. A photo of a dog, a selfie, a meme these all have high saturation.&lt;/p&gt;

&lt;p&gt;So before the image ever reaches the model, I convert it to &lt;strong&gt;HSV color space&lt;/strong&gt; and measure the mean saturation value. If it exceeds a threshold.&lt;/p&gt;

&lt;p&gt;The dog photo gets rejected now. The model only sees what it was trained to see.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Deployment Stack
&lt;/h2&gt;

&lt;p&gt;I wanted this to be completely free to host — no cloud bills, no server management.&lt;/p&gt;

&lt;p&gt;Here's what I ended up with:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model Inference&lt;/td&gt;
&lt;td&gt;Hugging Face Spaces (Gradio + TensorFlow-CPU)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;Netlify (custom Tailwind CSS portal)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bridge&lt;/td&gt;
&lt;td&gt;iframe&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Gradio app on Hugging Face handles all the heavy lifting  loading the &lt;code&gt;.h5&lt;/code&gt; model, running inference, returning JSON results. The Netlify frontend is just a clean portal that embeds the Gradio Space via iframe.&lt;/p&gt;

&lt;p&gt;Total hosting cost: &lt;strong&gt;$0.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Accuracy alone is a lie in medical AI.&lt;/strong&gt;&lt;br&gt;
A model can hit 90% accuracy while still missing dangerous cases. Always look at your confusion matrix. False negatives  missed pneumonia cases — are the ones that matter most.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Input validation is not optional.&lt;/strong&gt;&lt;br&gt;
Any model you deploy in the real world needs to handle unexpected inputs gracefully. The Saturation Gate wasn't in any tutorial I followed. I had to think of it myself after seeing the model fail in a funny but revealing way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Transfer learning is magic for small datasets.&lt;/strong&gt;&lt;br&gt;
If you're working with fewer than 50,000 images, you almost certainly shouldn't be training a CNN from scratch. Use pretrained weights. Freeze the base. Train only the head.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Ship first, improve later.&lt;/strong&gt;&lt;br&gt;
The first version was broken in obvious ways. But shipping it is what revealed those problems. The dog test only happened because I deployed it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grad-CAM heatmap overlays&lt;/strong&gt;  showing &lt;em&gt;which part&lt;/em&gt; of the X-ray triggered the detection. This is the standard in medical AI and would make the tool genuinely useful for educational purposes.
&lt;strong&gt;Precision/Recall analysis&lt;/strong&gt; properly evaluating false negative rate on the test set&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More pathologies&lt;/strong&gt; tuberculosis, pleural effusion, cardiomegaly&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Repo
&lt;/h2&gt;

&lt;p&gt;The trained model is open source. You can download it, use it, build on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/GradienNinja/PneumoScan-AI" rel="noopener noreferrer"&gt;github.com/GradienNinja/PneumoScan-AI&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;I fix cars for a living. I don't have a degree. I didn't take a course that handed me this project.&lt;/p&gt;

&lt;p&gt;I just built it.&lt;/p&gt;

&lt;p&gt;If you're self-taught and reading this wondering whether you're "ready" to build something real  you're not going to get ready by waiting. You get ready by shipping.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by Sheikh Sadi Asif — &lt;a href="https://github.com/GradienNinja" rel="noopener noreferrer"&gt;@GradienNinja&lt;/a&gt; | AstroLabSoft AI Lab&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Disclaimer: This is a research and educational project. Not a certified medical device. Always consult a qualified healthcare professional for medical decisions.*&lt;/li&gt;
&lt;/ul&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%2Faufslf4zi23mw26fe7bl.jpg" 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%2Faufslf4zi23mw26fe7bl.jpg" alt=" " width="800" height="1422"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>deeplearning</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
