Breast Cancer Detection Using CNN

Detect breast cancer using a 1D CNN in TensorFlow 2.0. Covers Conv1D, BatchNormalization, Dropout, Adam optimizer, and binary classification on medical data.

Sep 5, 2020Updated Jul 12, 202621 min readFollow

Topics You Will Master

Medical dataset loading and binary label encoding
Conv1D architecture stacked with BatchNormalization and Dropout
Adam optimizer and binary cross-entropy loss for medical data
Confusion matrix and ROC-AUC model evaluation

Breast Cancer Detection Using CNN in Python

Breast cancer is the most commonly occurring cancer in women and the second most common cancer overall. There were over 2 million new cases in 2018, making it a significant health problem in present days.

The key challenge in breast cancer detection is to classify tumors as malignant or benign. Malignant refers to cancer cells that can invade and kill nearby tissue and spread to other parts of the body. A benign tumor, unlike a malignant one, does not spread and is safer. Deep neural network techniques can improve the accuracy of early diagnosis a lot.

In this blog, we will build a 1D CNN in TensorFlow to classify tumors as malignant or benign using the Wisconsin diagnostic dataset.

tensorflow 2.3 is used to build the model. Install it with this command.

PLAINTEXT
!pip install tensorflow-gpu==2.3.0-rc0

Importing necessary library that will use in model building.

PYTHON
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Flatten, Dense, Dropout, BatchNormalization
from tensorflow.keras.layers import Conv1D, MaxPool1D

from tensorflow.keras.optimizers import Adam

print(tf.__version__)
OUTPUT
2.3.0

pandas for loading and manipulating the data.

NumPy is used for working with arrays. It also has functions for linear algebra, fourier transforms, and matrices.

pyplot from matplotlib is used to visualize the results.

Seaborn is a Python plotting library built on matplotlib. It makes it easy to draw clear statistical charts.

PYTHON
import pandas as pd
import numpy as np
import  seaborn as sns
import matplotlib.pyplot as plt
PYTHON
/usr/local/lib/python3.6/dist-packages/statsmodels/tools/_testing.py:19: FutureWarning: pandas.util.testing is deprecated. Use the functions in the public API at pandas.testing instead.
  import pandas.util.testing as tm
PYTHON
from sklearn import datasets, metrics
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

Load and return the breast cancer classification dataset. The breast cancer dataset is a classic and very easy binary classification dataset.

PYTHON
cancer = datasets.load_breast_cancer()

View any particular column with the help of cancer.DESCR.

PYTHON
print(cancer.DESCR)
PYTHON
.. _breast_cancer_dataset:

Breast cancer wisconsin (diagnostic) dataset
--------------------------------------------

**Data Set Characteristics:**

    :Number of Instances: 569

    :Number of Attributes: 30 numeric, predictive attributes and the class

    :Attribute Information:
        - radius (mean of distances from center to points on the perimeter)
        - texture (standard deviation of gray-scale values)
        - perimeter
        - area
        - smoothness (local variation in radius lengths)
        - compactness (perimeter^2 / area - 1.0)
        - concavity (severity of concave portions of the contour)
        - concave points (number of concave portions of the contour)
        - symmetry
        - fractal dimension ("coastline approximation" - 1)

        The mean, standard error, and "worst" or largest (mean of the three
        largest values) of these features were computed for each image,
        resulting in 30 features.  For instance, field 3 is Mean Radius, field
        13 is Radius SE, field 23 is Worst Radius.

        - class:
                - WDBC-Malignant
                - WDBC-Benign

    :Summary Statistics:

    ===================================== ====== ======
                                           Min    Max
    ===================================== ====== ======
    radius (mean):                        6.981  28.11
    texture (mean):                       9.71   39.28
    perimeter (mean):                     43.79  188.5
    area (mean):                          143.5  2501.0
    smoothness (mean):                    0.053  0.163
    compactness (mean):                   0.019  0.345
    concavity (mean):                     0.0    0.427
    concave points (mean):                0.0    0.201
    symmetry (mean):                      0.106  0.304
    fractal dimension (mean):             0.05   0.097
    radius (standard error):              0.112  2.873
    texture (standard error):             0.36   4.885
    perimeter (standard error):           0.757  21.98
    area (standard error):                6.802  542.2
    smoothness (standard error):          0.002  0.031
    compactness (standard error):         0.002  0.135
    concavity (standard error):           0.0    0.396
    concave points (standard error):      0.0    0.053
    symmetry (standard error):            0.008  0.079
    fractal dimension (standard error):   0.001  0.03
    radius (worst):                       7.93   36.04
    texture (worst):                      12.02  49.54
    perimeter (worst):                    50.41  251.2
    area (worst):                         185.2  4254.0
    smoothness (worst):                   0.071  0.223
    compactness (worst):                  0.027  1.058
    concavity (worst):                    0.0    1.252
    concave points (worst):               0.0    0.291
    symmetry (worst):                     0.156  0.664
    fractal dimension (worst):            0.055  0.208
    ===================================== ====== ======

    :Missing Attribute Values: None

    :Class Distribution: 212 - Malignant, 357 - Benign

A pandas DataFrame keeps all inputs and outputs together. The code below creates a dataframe from the cancer data and feature names.

PYTHON
X = pd.DataFrame(data = cancer.data, columns=cancer.feature_names)
X.head()
OUTPUT
mean radiusmean texturemean perimetermean areamean smoothnessmean compactnessmean concavitymean concave pointsmean symmetrymean fractal dimensionradius errortexture errorperimeter errorarea errorsmoothness errorcompactness errorconcavity errorconcave points errorsymmetry errorfractal dimension errorworst radiusworst textureworst perimeterworst areaworst smoothnessworst compactnessworst concavityworst concave pointsworst symmetryworst fractal dimension
017.9910.38122.801001.00.118400.277600.30010.147100.24190.078711.09500.90538.589153.400.0063990.049040.053730.015870.030030.00619325.3817.33184.602019.00.16220.66560.71190.26540.46010.11890
120.5717.77132.901326.00.084740.078640.08690.070170.18120.056670.54350.73393.39874.080.0052250.013080.018600.013400.013890.00353224.9923.41158.801956.00.12380.18660.24160.18600.27500.08902
219.6921.25130.001203.00.109600.159900.19740.127900.20690.059990.74560.78694.58594.030.0061500.040060.038320.020580.022500.00457123.5725.53152.501709.00.14440.42450.45040.24300.36130.08758
311.4220.3877.58386.10.142500.283900.24140.105200.25970.097440.49561.15603.44527.230.0091100.074580.056610.018670.059630.00920814.9126.5098.87567.70.20980.86630.68690.25750.66380.17300
420.2914.34135.101297.00.100300.132800.19800.104300.18090.058830.75720.78135.43894.440.0114900.024610.056880.018850.017560.00511522.5416.67152.201575.00.13740.20500.40000.16250.23640.07678
PYTHON
y = cancer.target
OUTPUT
y

array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
       0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0...])
PYTHON
cancer.target_names
OUTPUT
array(['malignant', 'benign'], dtype='<U9')
PYTHON
X.shape
OUTPUT
(569, 30)

Manual splitting is impractical, and random splitting is important for generalization. train_test_split from scikit-learn handles this, putting 80% of the data into training and 20% into testing.

PYTHON
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 0, stratify = y)
PYTHON
X_train.shape
OUTPUT
(455, 30)
PYTHON
X_test.shape
OUTPUT
(114, 30)

StandardScaler removes the mean and scales the data to unit variance.

PYTHON
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
PYTHON
X_train = X_train.reshape(455,30,1)
X_test = X_test.reshape(114, 30, 1)

Building the CNN Model

A Sequential() function is the easiest way to build a model in Keras. It lets us build a model layer by layer. Each layer has weights that correspond to the layer the follows it. Use the add() function to add layers to our model.

Conv1D() is a 1D convolution layer. It is good at pulling features from a fixed-length part of the data, when the exact spot of the feature does not matter. In the first Conv1D() layer, we learn 36 filters with a window size of 3. The input_shape gives the shape of the input, which the first layer of any neural network needs. We use the ReLU activation function here.

The Rectified Linear Unit (ReLU) is the most used activation function in deep learning. It returns 0 for any negative input, and for any positive value x it returns x. So we can write it as f(x)=max(0,x)

ReLU activation function graph showing zero output for negative inputs and linear output for positive values

To stop problem of shrinkage of data we use concept called Padding.

It has two types:

  • valid
  • same

Flattening is converting the data into a 1-dimensional array for inputting it to the next layer. The output of the convolutional layers is flattened to create a single long feature vector.

The Sigmoid function takes a value and returns another value between 0 and 1. It is non-linear and easy to work with. It is also smooth across all values of z and has a fixed output range.

Sigmoid activation function graph showing the S-curve mapping any input to a value between 0 and 1

PYTHON
epochs = 50
model = Sequential()
model.add(Conv1D(filters=32, kernel_size=2, activation='relu', input_shape = (30,1)))
model.add(BatchNormalization())
model.add(Dropout(0.2))

model.add(Conv1D(filters=64, kernel_size=2, activation='relu'))
model.add(BatchNormalization())
model.add(Dropout(0.5))

model.add(Flatten())
model.add(Dense(64, activation='relu'))
model.add(Dropout(0.5))

model.add(Dense(1, activation='sigmoid'))
PYTHON
model.summary()
PYTHON
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
conv1d (Conv1D)              (None, 29, 32)            96
_________________________________________________________________
batch_normalization (BatchNo (None, 29, 32)            128
_________________________________________________________________
dropout (Dropout)            (None, 29, 32)            0
_________________________________________________________________
conv1d_1 (Conv1D)            (None, 28, 64)            4160
_________________________________________________________________
batch_normalization_1 (Batch (None, 28, 64)            256
_________________________________________________________________
dropout_1 (Dropout)          (None, 28, 64)            0
_________________________________________________________________
flatten (Flatten)            (None, 1792)              0
_________________________________________________________________
dense (Dense)                (None, 64)                114752
_________________________________________________________________
dropout_2 (Dropout)          (None, 64)                0
_________________________________________________________________
dense_1 (Dense)              (None, 1)                 65
=================================================================
Total params: 119,457
Trainable params: 119,265
Non-trainable params: 192
_________________________________________________________________

Compile defines the loss function, the optimizer, and the metrics. That's all. It has nothing to do with the weights, and we can compile a model as many times as we want without causing any problem to pretrained weights.

PLAINTEXT
model.compile(optimizer=Adam(lr=0.00005), loss = 'binary_crossentropy', metrics=['accuracy'])

Trains the model for a fixed number of epochs (iterations on a dataset).

PYTHON
history = model.fit(X_train, y_train, epochs=epochs, validation_data=(X_test, y_test), verbose=1)
OUTPUT
...
Epoch 46/50
15/15 [==============================] - 0s 6ms/step - loss: 0.1054 - accuracy: 0.9560 - val_loss: 0.1064 - val_accuracy: 0.9649
Epoch 47/50
15/15 [==============================] - 0s 6ms/step - loss: 0.1373 - accuracy: 0.9473 - val_loss: 0.1074 - val_accuracy: 0.9649
Epoch 48/50
15/15 [==============================] - 0s 7ms/step - loss: 0.1078 - accuracy: 0.9538 - val_loss: 0.1068 - val_accuracy: 0.9649
Epoch 49/50
15/15 [==============================] - 0s 6ms/step - loss: 0.0896 - accuracy: 0.9648 - val_loss: 0.1060 - val_accuracy: 0.9649
Epoch 50/50
15/15 [==============================] - 0s 6ms/step - loss: 0.0927 - accuracy: 0.9648 - val_loss: 0.1047 - val_accuracy: 0.9649
PYTHON
def plot_learningCurve(history, epoch):
  # Plot training & validation accuracy values
  epoch_range = range(1, epoch+1)
  plt.plot(epoch_range, history.history['accuracy'])
  plt.plot(epoch_range, history.history['val_accuracy'])
  plt.title('Model accuracy')
  plt.ylabel('Accuracy')
  plt.xlabel('Epoch')
  plt.legend(['Train', 'Val'], loc='upper left')
  plt.show()

  # Plot training & validation loss values
  plt.plot(epoch_range, history.history['loss'])
  plt.plot(epoch_range, history.history['val_loss'])
  plt.title('Model loss')
  plt.ylabel('Loss')
  plt.xlabel('Epoch')
  plt.legend(['Train', 'Val'], loc='upper left')
  plt.show()

A history object that contains all information collected during training.

PYTHON
history.history
OUTPUT
{'accuracy': [0.6197802424430847, 0.7494505643844604, 0.795604407787323, 0.8461538553237915, 0.8395604491233826, 0.8593406677246094, 0.8901098966598511, 0.8791208863258362, 0.8813186883926392, 0.9098901152610779, 0.903296709060669, 0.9230769276618958, ...]}
PYTHON
plot_learningCurve(history, epochs)

In the Model accuracy graph, validation accuracy is always greater than train accuracy, which means the model is not overfitting.

In the Model accuracy graph, validation loss is also lower than training loss. The model can keep training until validation loss rises above training loss.

The 1D CNN successfully classifies breast cancer with good generalization on the Wisconsin diagnostic dataset.

Conclusion

In this blog, we built a 1D CNN to detect breast cancer using the Wisconsin diagnostic dataset. We split 569 samples into train and test sets and applied StandardScaler normalization. The two-block convolutional model trained with Adam reached about 96.5% test accuracy in 50 epochs.

Key takeaways:

  • A lightweight 1D CNN with Conv1D, BatchNormalization, and Dropout can achieve strong accuracy on small medical datasets without overfitting.
  • BatchNormalization stabilizes training by keeping layer activations near zero mean and unit variance, which lets us use a lower learning rate safely.
  • Validation accuracy consistently higher than training accuracy is a healthy sign: Dropout is working as intended, and the model generalizes well to unseen samples.

Next steps:

Found this useful? Keep building with me.

New tutorials every week on YouTube: or go deeper with a full structured course.

Find this tutorial useful?

Subscribe to our YouTube channels for more practical production walk-throughs.

Discussion & Comments