Web App for Pneumonia Detection. Build a simple web application to... | by Mandy Gu | Towards Data Science

All of the Python 3.7 code used can be found here. The article provides a detailed walk through of each step, but you can also follow the instructions in the repo’s README to spin up a local server. The Github repo also comes with pre-trained weights in case you want to skip the training process.
The goal of this article is to set up the framework with a simple model. There are tons of improvements that can be made to boost model performance!
We will be using labeled Chest X-Ray images to train a model for pneumonia detection. The dataset is hosted on Kaggle and consists of 5,863 X-Ray images. The pneumonia images are further categorized as viral or bacterial. Ignoring this secondary categorization, our model will classify images as pneumonia or normal.
One of the biggest challenges of using AI for disease diagnosis is the lack of labeled data. Data is not readily available and only medical professionals are qualified to label the data. Furthermore, data collection is often biased to specific demographics and the hospital equipment used to collect the data.
The images in the Pneumonia dataset were sourced from paediatric patients between 1–5 years old from Guangzhou Women and Children’s Medical Center. The diagnosis for these images was graded by “two expert physicians”.
There is a lot of variance between the imaging tools used by each hospital and the biological factors between patients of different race, sex and age. With these constraints, it is hard to understand how well our model can detect pneumonia in general.
We can spin up the model server without Docker, so installing Docker is completely optional. If you would like to use Docker and haven’t installed it yet, go here and follow the instructions for the operating system you are using.
The Kaggle API makes it easy to download Kaggle datasets programmatically/through the command line interface.
Create a Kaggle account if you don’t have one alreadyGenerate a new token at kaggle.com/USERNAME/accounts. This will prompt you to download a kaggle.json file which contains credentials for accessing the APIExport credentials from the json file as environment variables. The easiest way to do this is by running:
Create a Kaggle account if you don’t have one already
Generate a new token at kaggle.com/USERNAME/accounts. This will prompt you to download a kaggle.json file which contains credentials for accessing the API
Export credentials from the json file as environment variables. The easiest way to do this is by running:
export KAGGLE_USERNAME = [kaggle username]export KAGGLE_KEY = [generated_key]
The code on Github uses direnv, a shell extension for managing environment variables. If you prefer to use direnv to manage the Kaggle credentials, populate the .envrc file and run direnv allow in the directory to ingest the variables.
Run the good old pip install -r requirements.txt to download the Python dependencies.
You’ll find several instances of logging in the codebase. Logs are used similar to print statements in that they provide information, warnings and are helpful with debugging. Logging is preferred over printing in production environments as logs are more configurable (logs can be sent to a destination for storage/monitoring) and provide stack traces to determine where it came from.
The code used for data processing and model building will be contained in a class called Train.
When theforce_download parameter is set as true (or when no data folder is found), the script downloads and unzips the data from Kaggle.
When we authenticate the Kaggle API, it searches for the credentials in our environment variables. The next line downloads the dataset, which it identifies from self.dataset_name (paultimothymooney/chest-xray-pneumonia) and saves it to the specified path.
The dataset name is not always intuitive. We can find the name on the Kaggle UI by clicking the three vertical dots on the right and then Copy API command . The name can be inferred from the copied command.
The Kaggle dataset is downloaded into the following directory of jpeg images:
├── train│ ├── normal│ ├── pneumonia├── test│ ├── normal│ ├── pneumonia├── val│ ├── normal│ ├── pneumonia
We are going to use the ImageDataGenerator from Keras to load these images into memory. The data generator object makes it easy to load the data into memory and overcomes RAM limitations by loading the data in batches.
We define a separate generator for the train, test and validation set. The ImageDataGenerators come with an entire suite of preprocessing steps that can be applied to the images. For simplicity, we will only apply a rescale factor of 1/255.0.
In the generators, we specify that the target size is 64 by 64 pixels for each image and that the colour mode is grayscale. The data generators will infer the class labels based on the folder names.
Coloured images are typically represented using three channels (red, green and blue) while grayscale images are represented using one channel. These representations are stored inside nested arrays where each value represents the pixel’s hue for that particular channel. The pixel values range from 0 (black) to 255 (white).
Dividing each pixel by 255.0 is a commonly used method to normalize the data. This allows the classifier to converge faster (smaller values = faster computations). An alternative normalization technique to scaling is centring. We can centre our data by transforming it into a standard normal distribution.
For the purpose of this application, we will train our model from scratch. However, with less than 6000 images, this is not ideal. The complexity of image classification combined with the high dimensional feature space makes it easy to overfit classifiers trained on small datasets.
For that reason, transfer learning from pre-trained models is a very popular approach for small datasets.
In transfer learning, we take a complex, pre-trained model (such as VGG or ResNet) that learned the intricacies of standard object detection from large datasets. We freeze (essentially remove) the last few layers and retrain those layers against our smaller dataset. This allows the complex model to understand the distinction between chest X-rays.
We fit the model in batches using fit_generator .
The deployment step saves the model weights in a specific location in the same directory. For a larger scale project, this location would be centralized, likely hosted on a cloud storage service such as S3. In the next section, the app will restore the model using these weights for inference.
Line 3 reveals that the testing accuracy is 76%. There are 624 x-ray images in the test set (390 pneumonia, 234 normal). The f1 score is 72%.
For the record, I threw together a generic CNN for the model architecture, so it should be pretty easy to improve this performance.
We will be using FastAPI and Docker to deploy this application. I will be heavily paraphrasing from one of my previous articles on FastAPI for this section.
Routers are used to break up complex APIs into smaller pieces. Each router will have its own prefix to mark this separation. While this is not immediately necessary, I decided to use routers in case our API becomes more complex.
Create a folder called routers to hold the our routers. In the routers folder, define the API endpoint which outputs classification results after receiving X-ray images.
We shouldn’t require client applications to send numpy arrays to receive a classification output. That would be incredibly tedious for most applications to formulate.
Conveniently, FastAPI accepts File objects, which in this case would be a binary representation of the image. With the PILLOW library, we can convert the bytes to an numpy array.
In the same directory as therouters folder, create another script app.py. In this script, define the app and specify the router(s).
We also define a healthcheck function. A health check API returns the operational status of the application. Our health check returns the text Good to go when the application is healthy. Note that this invokes a 200 status code.
We can boot up our app locally by running this line in the command line:
uvicorn --host 0.0.0.0 --port 5000 classifier.app:app
The logging informs us that the server is running on http://0.0.0.0:5000
We can send POST requests to our server programatically, but that would require passing the byte rendition of the image as a parameter.
Conveniently, we can go to http://0.0.0.0:5000/docs for a friendlier UI.
Hit the green POST button, which will lead you to this page.
On the right hand side, click TRY IT OUT.
This will prompt you to upload an image. In the fixtures folder from the Github repo, you can find a couple of images that I pulled out from the training set (not to be used for judging model performance!). The name indicates the class.
I uploaded pneumonia_1.jpeg and received the following result:
Nice, we built a nifty local application for pneumonia detection!
The repo takes this a step further and adds a Dockerfile to build the application with Docker. Dockers encapsulates everything you need to build and run an application in one place, making it easier to share and deploy over a public URL.
If you cloned the repo, you can follow the instructions in the README to build and run the Docker container.
If you enjoyed this article, check out my other articles on Data Science, Math and Programming. Follow me on Medium for the latest updates. 😃
I am also building a comprehensive set of free Data Science lessons and practice problems at www.dscrashcourse.com as a hobby project.
If you want to support my writing, consider using my affiliate link the next time you sign up for a Coursera course. Full disclosure — I receive commission for every enrollment, but it comes at no extra cost for you.
Thank you again for reading! 📕