Post

Using PyTorch to identify respiratory problems through chest X-rays

Using PyTorch to identify respiratory problems through chest X-rays

Introduction

I have recently become interested in machine learning (ML) and the seemingly endless list of tasks which it can help with. Because of this, I have spent a good amount of time in the last few months learning it through tutorials, documentation, and side projects. When looking for some inspiration for what to make with PyTorch, I came across this Kaggle repository which had over 7,000 photos of chest x-rays from people with COVID-19, Tuberculosis, Pneumonia, or just a normal chest. This specifically caught my attention as it would be building a tool to accomplish a task that I am fully unable to do due to lacking the necessary background.

Process

Starting off, turning the data into tensors is a vital part of making an algorithm, but to make it more visual, I like to put an image of the data on coordinates to help visualize the number of pixels, and thus data within it.

Then, to ensure all pictures are the same size, and carry the same amount of data, all images must be transformed to the same size. Additionally, it is important to apply some transformations to create some diversity in the data. Then, the data needs to be turned into tensors which allows the model to understand the data. Below is how I made the transformations for the data.

1
2
3
4
5
6
7
data_transform = transforms.Compose([
    transforms.Resize(size=(224,224)), #changes the number of pixels  in the image to 224 x 224
    transforms.Lambda(lambda img: img.convert("RGB")), #makes sure all images have 3 colour channels
    transforms.RandomHorizontalFlip(p=0.5), #creates diversity in the data by flipping half of the images
    transforms.TrivialAugmentWide(num_magnitude_bins=31), # Further makes diversity by applying a series of random transformations
    transforms.ToTensor() #transforms the images into tensors
])

And this resulted in the following:

To prepare this data efficiently for the model, a data loader is needed. This is done fairly simply as PyTorch streamlines the process

1
2
3
4
5
6
7
# Create DataLoaders
BATCH_SIZE = 32
NUM_WORKERS = os.cpu_count()
train_dataloader_simple = DataLoader(train_data_simple,
                                     batch_size=BATCH_SIZE,
                                     shuffle=True,
                                     num_workers=NUM_WORKERS)

For the small convolutional neural network, it processes images through two blocks to extract the features, then flattens and passes it through a linear layer to give output predictions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def __init__(self, input_shape: int, hidden_units: int, output_shape: int, image_size: int) -> None:
        super().__init__()
        self.conv_block_1 = nn.Sequential( #First sequential layer assigned 
            nn.Conv2d(in_channels=input_shape, out_channels=hidden_units, kernel_size=3, stride=1, padding=1), # First convolutional layer
            nn.ReLU(), # (Rectified Linear Unit), adds non-linearity by zeroing out negative values
            nn.Conv2d(in_channels=hidden_units, out_channels=hidden_units, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2) # reduce spatial dimensions by half and moves 2 pixels  at a time
            #this basically summarizes the images for the model
        )
        self.conv_block_2 = nn.Sequential(
            nn.Conv2d(hidden_units, hidden_units, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(hidden_units, hidden_units, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2)
        )

        with torch.no_grad():
            dummy_input = torch.zeros(1, input_shape, image_size, image_size) # makes tensor full of 0's
            out = self.conv_block_1(dummy_input) #passes on the dummy input
            out = self.conv_block_2(out) #Passes the output from the first block through the second convolutional block 
            flatten_dim = out.view(1, -1).shape[1] #dynamically calculate how many input features should be expected

        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(in_features=flatten_dim, out_features=output_shape)
        )

    def forward(self, x: torch.Tensor):
        x = self.conv_block_1(x)
        x = self.conv_block_2(x)
        x = self.classifier(x)
        return x #passes the value through the conv layers and returns the output

After this, the model must step forward and repeat to continue working properly. This requires the loss to be accumulated, an optimizer applied (zero_grad in this case), backpropagation, and an optimizer step where the bias and weight are updated.

Training and results

By setting a number of Epochs, the model can be trained and worked through that many times. This model took about ten minutes to train on Google Colab T4 GPUs and resulted in an accuracy of 81.25%. Below, a few examples of results and confidence are shown.

To see why the results had some error, I graphed the loss and accuracy and found a slight tendency to overfit, which may be caused by the lack of variation in the data, which could be adjusted in the future. Despite this, the overall trend seems to be going in the right direction.

Going forward, the optimizer method should be experimented with to see if the overall accuracy can be improved. On top of this, larger picture sizes would allow a greater level of detail to be maintained which could also serve to increase the accuracy. However, the great thing about ML is how it encourages experimentation and trial and error, as that is often the best way forward when there is no specific path.

This post is licensed under CC BY 4.0 by the author.