PHÂN LOẠI CHẨN ĐOÁN VIÊM PHỔI TRÊN X QUANG VỚI PYTHON TENSORFLOW-KERAS
Nguyen Ngoc Thieu August 2nd, 2018
Đây là một dự án trên trang Kaggle, nhằm tìm ra những model tối ưu trong phân loại chẩn đoán viêm phổi dựa trên phim X quang.
Hiện có vài tác giả tham gia viết những scripts trên R và cả Python để phân loại chẩn đoán viêm phổi, không viêm phổi dựa trên nhiều phim X quang đã được chẩn đoán (train), để rồi kiểm định trên những phim trong nhóm test.
Nếu sao chép hoàn toàn những scripts đã có để thực hành, chúng ta sẽ gặp nhiều lỗi do không tương thích với CPU, GPU mà chúng ta hiện có. Hơn nữa, các tác giả này viết code rất “cao siêu”, sử dụng những file viết sẳn tôi cảm thấy khó nắm bắt được.
Học tập cách đọc dữ liệu hình ảnh từ những bài viết này và phương thức xây dựng các lớp Keras, tôi có viết lại Scripts để phù hợp với GPU và kiến thức còn hạn chế của mình trong khi thực hành dự án này.
Kết quả tóm tắt là:
Loss on test set: 1.25
Accuracy on test set: 0.87
Recall of the model is 0.96
Precision of the model is 0.84
Nếu có một máy tính với CPU, GPU mạnh hơn, chúng ta có thể xử lí hình ảnh đầu vào với size của hình ảnh lớn hơn trong dự án này (28x28 pixels), thì tính chính xác của mô hình có thể còn được cải thiện hơn.
Mục đích là học tập cách đọc dữ liệu hình ảnh, phân tích dữ liệu và xây dựng các mô hình chẩn đoán hình ảnh với Tensorflow-Keras. Scripts dưới đây viết với Python.
Xin cám ơn Bs Khả Nhi, bạn Đức Anh Lê, Khánh Đình Phạm đã có những gợi ý giúp tôi trong bài viết ngắn này.
Mong nhận được những góp ý của các bạn để cùng học tập và tiến bộ.
HÌNH ẢNH PHIM X QUANG PHỔI:
import tensorflow as tf
import os
import glob
import shutil
import cv2
import imgaug as aug
import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)
import seaborn as sns
import matplotlib.pyplot as plt
import matplotlib.image as mimg
import imgaug.augmenters as iaa
from os import listdir, makedirs, getcwd, remove
from os.path import isfile, join, abspath, exists, isdir, expanduser
from PIL import Image
from pathlib import Path
from skimage.io import imread
from skimage.transform import resize
from keras.models import Sequential, Model
from keras.preprocessing.image import ImageDataGenerator,load_img, img_to_array
from keras.models import Sequential
from keras.layers import Conv2D,Convolution2D, MaxPooling2D, Dense, Dropout, Input, Flatten, SeparableConv2D
from keras.layers import GlobalMaxPooling2D
from keras.layers.normalization import BatchNormalization
from keras.layers.merge import Concatenate
from keras.models import Model
from keras.optimizers import Adam, SGD, RMSprop
from keras.callbacks import ModelCheckpoint, Callback, EarlyStopping
from keras.utils import to_categorical
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from mlxtend.plotting import plot_confusion_matrix
from sklearn.metrics import confusion_matrix
from keras import backend as K
color = sns.color_palette()
config = tf.ConfigProto(allow_soft_placement=True)
config.gpu_options.allocator_type = 'BFC'
config.gpu_options.per_process_gpu_memory_fraction = 0.40
config.gpu_options.allow_growth = True
# Set the seed for hash based operations in python
os.environ['PYTHONHASHSEED'] = '0'
# Set the numpy seed
np.random.seed(111)
# Disable multi-threading in tensorflow ops
session_conf = tf.ConfigProto(intra_op_parallelism_threads=1, inter_op_parallelism_threads=1)
# Set the random seed in tensorflow at graph level
tf.set_random_seed(111)
# Define a tensorflow session with above session configs
#sess = tf.Session(graph=tf.get_default_graph(), config=session_conf)
sess = tf.Session(config=tf.ConfigProto(log_device_placement=True))
# Set the session in keras
K.set_session(sess)
# Make the augmentation sequence deterministic
aug.seed(111)
# Input data files are available in the "D:/Image-Analysis/Data/Data2/" directory.
print(os.listdir("D:/Image-Analysis/Data/Data2/"))
# Define path to the data directory
data_dir = Path('D:/Image-Analysis/Data/Data2')
# Path to train directory (Fancy pathlib...no more os.path!!)
train_dir = data_dir / 'train'
# Path to validation directory
val_dir = data_dir / 'val'
# Path to test directory
test_dir = data_dir / 'test'
# Get the path to the normal and pneumonia sub-directories
normal_cases_dir = train_dir / 'NORMAL'
pneumonia_cases_dir = train_dir / 'PNEUMONIA'
# Get the list of all the images
normal_cases = normal_cases_dir.glob('*.jpeg')
pneumonia_cases = pneumonia_cases_dir.glob('*.jpeg')
['best_model', 'best_model.hdf5', 'chest-xray-pneumonia.zip', 'chest_xray.zip', 'images_001.tar.gz', 'JPG-PNG-to-MNIST-NN-Format-master', 'JPG-PNG-to-MNIST-NN-Format-master.zip', 'test', 'train', 'val', 'vgg16']
# Preparing train data
# Get the path to the normal and pneumonia sub-directories
normal_cases_dir = train_dir / 'NORMAL'
pneumonia_cases_dir = train_dir / 'PNEUMONIA'
# Get the list of all the images
normal_cases = normal_cases_dir.glob('*.jpeg')
pneumonia_cases = pneumonia_cases_dir.glob('*.jpeg')
# An empty list. We will insert the data into this list in (img_path, label) format
train_data = []
train_labels = []
for img in normal_cases:
img = cv2.imread(str(img))
img = cv2.resize(img, (28,28))
if img.shape[2] ==1:
img = np.dstack([img, img, img])
else:
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = img.astype(np.float32)/255.
label = to_categorical(0, num_classes=2)
train_data.append(img)
train_labels.append(label)
for img in pneumonia_cases:
img = cv2.imread(str(img))
img = cv2.resize(img, (28,28))
if img.shape[2] ==1:
img = np.dstack([img, img, img])
else:
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = img.astype(np.float32)/255.
label = to_categorical(1, num_classes=2)
train_data.append(img)
train_labels.append(label)
train_data = np.array(train_data)
train_labels = np.array(train_labels)
print(np.shape(train_data))
print(np.shape(train_labels))
(5216, 28, 28, 3)
(5216, 2)
# Preparing test data
normal_cases_dir = test_dir / 'NORMAL'
pneumonia_cases_dir = test_dir / 'PNEUMONIA'
normal_cases = normal_cases_dir.glob('*.jpeg')
pneumonia_cases = pneumonia_cases_dir.glob('*.jpeg')
test_data = []
test_labels = []
for img in normal_cases:
img = cv2.imread(str(img))
img = cv2.resize(img, (28,28))
if img.shape[2] ==1:
img = np.dstack([img, img, img])
else:
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = img.astype(np.float32)/255.
label = to_categorical(0, num_classes=2)
test_data.append(img)
test_labels.append(label)
for img in pneumonia_cases:
img = cv2.imread(str(img))
img = cv2.resize(img, (28,28))
if img.shape[2] ==1:
img = np.dstack([img, img, img])
else:
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = img.astype(np.float32)/255.
label = to_categorical(1, num_classes=2)
test_data.append(img)
test_labels.append(label)
test_data = np.array(test_data)
test_labels = np.array(test_labels)
print("Total number of test examples: ", test_data.shape)
print("Total number of labels:", test_labels.shape)
Total number of test examples: (624, 28, 28, 3)
Total number of labels: (624, 2)
# Building the model
model = Sequential()
model.add(Conv2D(64, (3, 3), activation='relu', input_shape=(28,28,3)))
model.add(Convolution2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(SeparableConv2D(128, (3,3), activation='relu', padding='same'))
model.add(SeparableConv2D(128, (3,3), activation='relu', padding='same'))
model.add(MaxPooling2D(2,2))
model.add(SeparableConv2D(256, (3,3), activation='relu', padding='same'))
model.add(BatchNormalization())
model.add(SeparableConv2D(256, (3,3), activation='relu', padding='same'))
model.add(BatchNormalization())
model.add(SeparableConv2D(256, (3,3), activation='relu', padding='same'))
model.add(MaxPooling2D((2,2)))
model.add(SeparableConv2D(512, (3,3), activation='relu', padding='same'))
model.add(BatchNormalization())
model.add(SeparableConv2D(512, (3,3), activation='relu', padding='same'))
model.add(BatchNormalization())
model.add(SeparableConv2D(512, (3,3), activation='relu', padding='same'))
model.add(MaxPooling2D((2,2)))
model.add(Flatten())
model.add(Dense(1024, activation='relu'))
model.add(Dropout(0.7))
model.add(Dense(512, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(2, activation='softmax'))
print(model.output_shape)
print(model.summary())
(None, 2)
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_1 (Conv2D) (None, 26, 26, 64) 1792
_________________________________________________________________
conv2d_2 (Conv2D) (None, 24, 24, 64) 36928
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 12, 12, 64) 0
_________________________________________________________________
separable_conv2d_1 (Separabl (None, 12, 12, 128) 8896
_________________________________________________________________
separable_conv2d_2 (Separabl (None, 12, 12, 128) 17664
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 6, 6, 128) 0
_________________________________________________________________
separable_conv2d_3 (Separabl (None, 6, 6, 256) 34176
_________________________________________________________________
batch_normalization_1 (Batch (None, 6, 6, 256) 1024
_________________________________________________________________
separable_conv2d_4 (Separabl (None, 6, 6, 256) 68096
_________________________________________________________________
batch_normalization_2 (Batch (None, 6, 6, 256) 1024
_________________________________________________________________
separable_conv2d_5 (Separabl (None, 6, 6, 256) 68096
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 3, 3, 256) 0
_________________________________________________________________
separable_conv2d_6 (Separabl (None, 3, 3, 512) 133888
_________________________________________________________________
batch_normalization_3 (Batch (None, 3, 3, 512) 2048
_________________________________________________________________
separable_conv2d_7 (Separabl (None, 3, 3, 512) 267264
_________________________________________________________________
batch_normalization_4 (Batch (None, 3, 3, 512) 2048
_________________________________________________________________
separable_conv2d_8 (Separabl (None, 3, 3, 512) 267264
_________________________________________________________________
max_pooling2d_4 (MaxPooling2 (None, 1, 1, 512) 0
_________________________________________________________________
flatten_1 (Flatten) (None, 512) 0
_________________________________________________________________
dense_1 (Dense) (None, 1024) 525312
_________________________________________________________________
dropout_1 (Dropout) (None, 1024) 0
_________________________________________________________________
dense_2 (Dense) (None, 512) 524800
_________________________________________________________________
dropout_2 (Dropout) (None, 512) 0
_________________________________________________________________
dense_3 (Dense) (None, 2) 1026
=================================================================
Total params: 1,961,346
Trainable params: 1,958,274
Non-trainable params: 3,072
_________________________________________________________________
None
# Compiling the model
model.compile(loss='categorical_crossentropy',optimizer='adam',metrics=['accuracy'])
# Fitting the model
model.fit(train_data, train_labels, batch_size=20, epochs=10, verbose=1)
model.fit(train_data, train_labels, batch_size=20, epochs=10, verbose=1)
Epoch 1/10 5216/5216 [==============================] - 8s 1ms/step - loss: 0.1005 - acc: 0.9638
Epoch 2/10 5216/5216 [==============================] - 7s 1ms/step - loss: 0.0809 - acc: 0.9726
Epoch 3/10 5216/5216 [==============================] - 7s 1ms/step - loss: 0.0875 - acc: 0.9705
Epoch 4/10 5216/5216 [==============================] - 7s 1ms/step - loss: 0.0799 - acc: 0.9734
Epoch 5/10 5216/5216 [==============================] - 7s 1ms/step - loss: 0.0847 - acc: 0.9712
Epoch 6/10 5216/5216 [==============================] - 7s 1ms/step - loss: 0.0654 - acc: 0.9772
Epoch 7/10 5216/5216 [==============================] - 7s 1ms/step - loss: 0.0725 - acc: 0.9753
Epoch 8/10 5216/5216 [==============================] - 7s 1ms/step - loss: 0.0645 - acc: 0.9785
Epoch 9/10 5216/5216 [==============================] - 7s 1ms/step - loss: 0.0633 - acc: 0.9770
Epoch 10/10 5216/5216 [==============================] - 7s 1ms/step - loss: 0.0437 - acc: 0.9826
Out[29]: <keras.callbacks.History at 0x1aa0d52a908>
# Evaluating the model
test_loss, test_score = model.evaluate(test_data, test_labels, batch_size=20)
print("Loss on test set: ", test_loss)
print("Accuracy on test set: ", test_score)
test_loss, test_score = model.evaluate(test_data, test_labels, batch_size=20) print(“Loss on test set:”, test_loss) print(“Accuracy on test set:”, test_score) 624/624
[==============================] - 0s 443us/step
Loss on test set: 1.2491534859720468 Accuracy on test set: 0.866987176430531
# Get predictions
preds = model.predict(test_data, batch_size=20)
preds = np.argmax(preds, axis=-1)
# Original labels
orig_test_labels = np.argmax(test_labels, axis=-1)
print(orig_test_labels.shape)
print(preds.shape)# Get the confusion matrix
cm = confusion_matrix(orig_test_labels, preds)
plt.figure()
plot_confusion_matrix(cm,figsize=(12,8), hide_ticks=True, alpha=0.7,cmap=plt.cm.Blues)
plt.xticks(range(2), ['Normal', 'Pneumonia'], fontsize=16)
plt.yticks(range(2), ['Normal', 'Pneumonia'], fontsize=16)
plt.show()
# Calculate Precision and Recall
tn, fp, fn, tp = cm.ravel()
precision = tp/(tp+fp)
recall = tp/(tp+fn)
print("Recall of the model is {:.2f}".format(recall))
print("Precision of the model is {:.2f}".format(precision))
tn, fp, fn, tp = cm.ravel()
precision = tp/(tp+fp) recall = tp/(tp+fn)
print(“Recall of the model is {:.2f}”.format(recall)) print(“Precision of the model is {:.2f}”.format(precision))
Recall of the model is 0.96 Precision of the model is 0.84