使用深度学习检测疟疾

人工智能与开源工具相结合,可以改善致命疾病疟疾的诊断。
297 位读者喜欢这篇文章。
open source doctor

Opensource.com

人工智能 (AI) 和开源工具、技术及框架是改善社会的强大组合。“健康是财富” 这句话也许是老生常谈,但它非常准确!在本文中,我们将探讨如何利用人工智能,通过低成本、有效且准确的开源深度学习解决方案来检测致命疾病疟疾。

虽然我既不是医生,也不是医疗保健研究人员,我的资历也远不及他们,但我对将人工智能应用于医疗保健研究很感兴趣。我写这篇文章的目的是展示人工智能和开源解决方案如何帮助疟疾检测并减少人工劳动。

Python and TensorFlow

Python 和 TensorFlow — 构建开源深度学习解决方案的绝佳组合

得益于 Python 的强大功能和 TensorFlow 等深度学习框架,我们可以构建稳健、可扩展且有效的深度学习解决方案。由于这些工具是免费和开源的,我们可以构建非常经济高效的解决方案,并且可以被任何人轻松采用和使用。让我们开始吧!

项目动机

疟疾是一种致命的、传染性的蚊媒疾病,由疟原虫寄生虫引起,通过受感染的雌性按蚊叮咬传播。有五种寄生虫会导致疟疾,但有两种类型——恶性疟原虫间日疟原虫——引起了大多数病例。

这张地图显示疟疾在全球范围内流行,尤其是在热带地区,但该疾病的性质和致命性是该项目的主要动机。

如果被感染的蚊子叮咬,蚊子携带的寄生虫会进入您的血液并开始破坏携带氧气的红细胞 (RBC)。通常,疟疾的最初症状类似于流感等病毒,并且通常在蚊子叮咬后的几天或几周内开始出现。然而,这些致命的寄生虫可以在您的体内存活一年多而没有引起症状,延误治疗可能会导致并发症甚至死亡。因此,早期检测可以挽救生命。

世界卫生组织 (WHO) 的 疟疾概况 表明,全球近一半的人口面临疟疾风险,每年有超过 2 亿疟疾病例和约 40 万人死于疟疾。这是使疟疾检测和诊断变得快速、简便和有效的动力。

疟疾检测方法

有几种方法可用于疟疾检测和诊断。我们的项目所依据的论文《预训练卷积神经网络作为特征提取器,以改进薄血涂片图像中的疟疾寄生虫检测》(作者:Rajaraman 等人)介绍了一些方法,包括聚合酶链反应 (PCR) 和快速诊断测试 (RDT)。这两种测试通常在高品质显微镜服务不易获得的情况下使用。

根据 Carlos Ariza 的文章《疟疾英雄:用于更快疟疾诊断的 Web 应用程序》,标准的疟疾诊断通常基于血涂片工作流程。这篇文章是我在 Adrian Rosebrock 的《使用 Keras 进行深度学习和医学图像分析》中了解到的。我感谢这些优秀资源的作者让我更深入地了解了疟疾的流行、诊断和治疗。

Blood smear workflow for Malaria detection

用于疟疾检测的血涂片工作流程。

根据世卫组织协议,诊断通常包括在 100 倍放大倍率下对血涂片进行深入检查。训练有素的人员手动计算 5,000 个细胞中含有寄生虫的红细胞数量。正如上面引用的 Rajaraman 等人的论文所解释的那样

厚血涂片有助于检测寄生虫的存在,而薄血涂片有助于识别引起感染的寄生虫种类(美国疾病控制与预防中心,2012 年)。诊断准确性在很大程度上取决于人类专业知识,并且可能受到观察者间变异性和在疾病流行/资源受限地区进行大规模诊断的责任的影响(Mitiku、Mengistu 和 Gelaw,2003 年)。聚合酶链反应 (PCR) 和快速诊断测试 (RDT) 等替代技术也被使用;然而,PCR 分析的性能有限(Hommelsheim 等人,2014 年),并且 RDT 在疾病流行地区的成本效益较低(Hawkes、Katsuva 和 Masumbuko,2009 年)。

因此,疟疾检测可以从使用深度学习的自动化中获益。

用于疟疾检测的深度学习

血涂片的手动诊断是一项密集的手工过程,需要分类和计数寄生细胞和未感染细胞的专业知识。此过程可能无法很好地扩展,尤其是在难以找到合适专业知识的地区。在利用最先进的图像处理和分析技术来提取手工工程特征并构建基于机器学习的分类模型方面,已经取得了一些进展。然而,考虑到有更多数据可用于训练以及手工工程特征需要花费大量时间这一事实,这些模型无法扩展。

深度学习模型,更具体地说是卷积神经网络 (CNN),已被证明在各种计算机视觉任务中非常有效。(如果您想了解有关 CNN 的更多背景知识,我建议您阅读 CS231n 视觉识别卷积神经网络。)简而言之,CNN 模型中的关键层包括卷积层和池化层,如下图所示。

A typical CNN architecture

典型的 CNN 架构。

卷积层从数据中学习空间分层模式,这些模式也是平移不变的,因此它们能够学习图像的不同方面。例如,第一个卷积层将学习小的和局部的模式,例如边缘和角,第二个卷积层将基于来自第一层的特征学习更大的模式,依此类推。这使得 CNN 能够自动化特征工程并学习在新的数据点上泛化良好的有效特征。池化层有助于下采样和降维。

因此,CNN 有助于自动化和可扩展的特征工程。此外,在模型末尾插入密集层使我们能够执行图像分类等任务。使用像 CNN 这样的深度学习模型进行自动化疟疾检测可能非常有效、廉价且可扩展,尤其是在迁移学习和预训练模型兴起之后,即使在数据较少等约束条件下,这些模型也能很好地工作。

Rajaraman 等人的论文利用数据集上的六个预训练模型,在检测疟疾与未感染样本方面获得了令人印象深刻的 95.9% 的准确率。我们的重点是尝试一些简单的从头开始的 CNN 模型和几个使用迁移学习的预训练模型,看看我们可以在相同数据集上获得的结果。我们将使用开源工具和框架,包括 Python 和 TensorFlow,来构建我们的模型。

数据集

我们分析的数据来自美国国家医学图书馆 (NLM) 下属的 Lister Hill 国家生物医学通信中心 (LHNCBC) 的研究人员,他们精心收集并注释了健康和感染的血涂片图像的 公开数据集。这些研究人员开发了一个移动 疟疾检测应用程序,该应用程序可在连接到传统光学显微镜的标准 Android 智能手机上运行。他们使用了来自 150 名恶性疟原虫感染患者和 50 名健康患者的吉姆萨染色薄血涂片,这些涂片是在孟加拉国吉大港医学院医院收集和拍摄的。智能手机的内置摄像头采集了每个显微镜视野的载玻片图像。这些图像由泰国曼谷 Mahidol-Oxford 热带医学研究部门的专家载玻片阅读器手动注释。

让我们简要了解一下数据集的结构。首先,我将安装一些基本依赖项(基于正在使用的操作系统)。

Installing dependencies

我正在云端使用基于 Debian 的系统,并配备 GPU,这样我可以更快地运行我的模型。要查看目录结构,我们必须使用 sudo apt install tree 安装 tree 依赖项(如果我们没有安装)。

Installing the tree dependency

我们有两个文件夹,其中包含细胞图像,分别是感染细胞和健康细胞。我们可以通过输入以下命令来获取有关图像总数的更多详细信息

import os
import glob

base_dir = os.path.join('./cell_images')
infected_dir = os.path.join(base_dir,'Parasitized')
healthy_dir = os.path.join(base_dir,'Uninfected')

infected_files = glob.glob(infected_dir+'/*.png')
healthy_files = glob.glob(healthy_dir+'/*.png')
len(infected_files), len(healthy_files)

# Output
(13779, 13779)

看起来我们有一个平衡的数据集,其中包含 13,779 张疟疾细胞图像和 13,779 张非疟疾(未感染)细胞图像。让我们从中构建一个数据框,我们将在开始构建数据集时使用它。

import numpy as np
import pandas as pd

np.random.seed(42)

files_df = pd.DataFrame({
    'filename': infected_files + healthy_files,
    'label': ['malaria'] * len(infected_files) + ['healthy'] * len(healthy_files)
}).sample(frac=1, random_state=42).reset_index(drop=True)

files_df.head()

Datasets

构建和探索图像数据集

为了构建深度学习模型,我们需要训练数据,但我们也需要在未见过的数据上测试模型的性能。我们将使用 60:10:30 的比例分别用于训练、验证和测试数据集。我们将在训练期间利用训练和验证数据集,并在测试数据集上检查模型的性能。

from sklearn.model_selection import train_test_split
from collections import Counter

train_files, test_files, train_labels, test_labels = train_test_split(files_df['filename'].values,
                                                                      files_df['label'].values, 
                                                                      test_size=0.3, random_state=42)
train_files, val_files, train_labels, val_labels = train_test_split(train_files,
                                                                    train_labels, 
                                                                    test_size=0.1, random_state=42)

print(train_files.shape, val_files.shape, test_files.shape)
print('Train:', Counter(train_labels), '\nVal:', Counter(val_labels), '\nTest:', Counter(test_labels))

# Output
(17361,) (1929,) (8268,)
Train: Counter({'healthy': 8734, 'malaria': 8627}) 
Val: Counter({'healthy': 970, 'malaria': 959}) 
Test: Counter({'malaria': 4193, 'healthy': 4075})

图像的尺寸将不相等,因为血涂片和细胞图像会因人、测试方法和照片的方向而异。让我们获取训练数据集的一些摘要统计信息,以确定最佳图像尺寸(请记住,我们根本不会触及测试数据集!)。

import cv2
from concurrent import futures
import threading

def get_img_shape_parallel(idx, img, total_imgs):
    if idx % 5000 == 0 or idx == (total_imgs - 1):
        print('{}: working on img num: {}'.format(threading.current_thread().name,
                                                  idx))
    return cv2.imread(img).shape
  
ex = futures.ThreadPoolExecutor(max_workers=None)
data_inp = [(idx, img, len(train_files)) for idx, img in enumerate(train_files)]
print('Starting Img shape computation:')
train_img_dims_map = ex.map(get_img_shape_parallel, 
                            [record[0] for record in data_inp],
                            [record[1] for record in data_inp],
                            [record[2] for record in data_inp])
train_img_dims = list(train_img_dims_map)
print('Min Dimensions:', np.min(train_img_dims, axis=0)) 
print('Avg Dimensions:', np.mean(train_img_dims, axis=0))
print('Median Dimensions:', np.median(train_img_dims, axis=0))
print('Max Dimensions:', np.max(train_img_dims, axis=0))


# Output
Starting Img shape computation:
ThreadPoolExecutor-0_0: working on img num: 0
ThreadPoolExecutor-0_17: working on img num: 5000
ThreadPoolExecutor-0_15: working on img num: 10000
ThreadPoolExecutor-0_1: working on img num: 15000
ThreadPoolExecutor-0_7: working on img num: 17360
Min Dimensions: [46 46  3]
Avg Dimensions: [132.77311215 132.45757733   3.]
Median Dimensions: [130. 130.   3.]
Max Dimensions: [385 394   3]

我们应用并行处理来加速图像读取操作,并根据摘要统计信息,我们将每个图像调整为 125x125 像素。让我们加载所有图像并将它们调整为这些固定尺寸。

IMG_DIMS = (125, 125)

def get_img_data_parallel(idx, img, total_imgs):
    if idx % 5000 == 0 or idx == (total_imgs - 1):
        print('{}: working on img num: {}'.format(threading.current_thread().name,
                                                  idx))
    img = cv2.imread(img)
    img = cv2.resize(img, dsize=IMG_DIMS, 
                     interpolation=cv2.INTER_CUBIC)
    img = np.array(img, dtype=np.float32)
    return img

ex = futures.ThreadPoolExecutor(max_workers=None)
train_data_inp = [(idx, img, len(train_files)) for idx, img in enumerate(train_files)]
val_data_inp = [(idx, img, len(val_files)) for idx, img in enumerate(val_files)]
test_data_inp = [(idx, img, len(test_files)) for idx, img in enumerate(test_files)]

print('Loading Train Images:')
train_data_map = ex.map(get_img_data_parallel, 
                        [record[0] for record in train_data_inp],
                        [record[1] for record in train_data_inp],
                        [record[2] for record in train_data_inp])
train_data = np.array(list(train_data_map))

print('\nLoading Validation Images:')
val_data_map = ex.map(get_img_data_parallel, 
                        [record[0] for record in val_data_inp],
                        [record[1] for record in val_data_inp],
                        [record[2] for record in val_data_inp])
val_data = np.array(list(val_data_map))

print('\nLoading Test Images:')
test_data_map = ex.map(get_img_data_parallel, 
                        [record[0] for record in test_data_inp],
                        [record[1] for record in test_data_inp],
                        [record[2] for record in test_data_inp])
test_data = np.array(list(test_data_map))

train_data.shape, val_data.shape, test_data.shape  


# Output
Loading Train Images:
ThreadPoolExecutor-1_0: working on img num: 0
ThreadPoolExecutor-1_12: working on img num: 5000
ThreadPoolExecutor-1_6: working on img num: 10000
ThreadPoolExecutor-1_10: working on img num: 15000
ThreadPoolExecutor-1_3: working on img num: 17360

Loading Validation Images:
ThreadPoolExecutor-1_13: working on img num: 0
ThreadPoolExecutor-1_18: working on img num: 1928

Loading Test Images:
ThreadPoolExecutor-1_5: working on img num: 0
ThreadPoolExecutor-1_19: working on img num: 5000
ThreadPoolExecutor-1_8: working on img num: 8267
((17361, 125, 125, 3), (1929, 125, 125, 3), (8268, 125, 125, 3))

我们再次利用并行处理来加速与图像加载和调整大小相关的计算。最后,我们获得了所需尺寸的图像张量,如前面的输出所示。我们现在可以查看一些样本细胞图像,以了解我们的数据外观。

import matplotlib.pyplot as plt
%matplotlib inline

plt.figure(1 , figsize = (8 , 8))
n = 0 
for i in range(16):
    n += 1 
    r = np.random.randint(0 , train_data.shape[0] , 1)
    plt.subplot(4 , 4 , n)
    plt.subplots_adjust(hspace = 0.5 , wspace = 0.5)
    plt.imshow(train_data[r[0]]/255.)
    plt.title('{}'.format(train_labels[r[0]]))
    plt.xticks([]) , plt.yticks([])

Malaria cell samples

根据这些样本图像,我们可以看到疟疾细胞图像和健康细胞图像之间存在一些细微差异。我们将让我们的深度学习模型尝试在模型训练期间学习这些模式。

在我们开始训练模型之前,我们必须设置一些基本配置设置。

BATCH_SIZE = 64
NUM_CLASSES = 2
EPOCHS = 25
INPUT_SHAPE = (125, 125, 3)

train_imgs_scaled = train_data / 255.
val_imgs_scaled = val_data / 255.

# encode text category labels
from sklearn.preprocessing import LabelEncoder

le = LabelEncoder()
le.fit(train_labels)
train_labels_enc = le.transform(train_labels)
val_labels_enc = le.transform(val_labels)

print(train_labels[:6], train_labels_enc[:6])


# Output
['malaria' 'malaria' 'malaria' 'healthy' 'healthy' 'malaria'] [1 1 1 0 0 1]

我们固定了图像尺寸、批次大小和 epoch,并编码了我们的分类类标签。TensorFlow 2.0 的 alpha 版本于 2019 年 3 月发布,此练习是尝试它的绝佳借口。

import tensorflow as tf

# Load the TensorBoard notebook extension (optional)
%load_ext tensorboard.notebook

tf.random.set_seed(42)
tf.__version__

# Output
'2.0.0-alpha0'

深度学习模型训练

在模型训练阶段,我们将构建三个深度学习模型,使用我们的训练数据训练它们,并使用验证数据比较它们的性能。然后,我们将保存这些模型,并在以后的模型评估阶段中使用它们。

模型 1:从头开始的 CNN

我们的第一个疟疾检测模型将从头开始构建和训练一个基本的 CNN。首先,让我们定义我们的模型架构。

inp = tf.keras.layers.Input(shape=INPUT_SHAPE)

conv1 = tf.keras.layers.Conv2D(32, kernel_size=(3, 3), 
                               activation='relu', padding='same')(inp)
pool1 = tf.keras.layers.MaxPooling2D(pool_size=(2, 2))(conv1)
conv2 = tf.keras.layers.Conv2D(64, kernel_size=(3, 3), 
                               activation='relu', padding='same')(pool1)
pool2 = tf.keras.layers.MaxPooling2D(pool_size=(2, 2))(conv2)
conv3 = tf.keras.layers.Conv2D(128, kernel_size=(3, 3), 
                               activation='relu', padding='same')(pool2)
pool3 = tf.keras.layers.MaxPooling2D(pool_size=(2, 2))(conv3)

flat = tf.keras.layers.Flatten()(pool3)

hidden1 = tf.keras.layers.Dense(512, activation='relu')(flat)
drop1 = tf.keras.layers.Dropout(rate=0.3)(hidden1)
hidden2 = tf.keras.layers.Dense(512, activation='relu')(drop1)
drop2 = tf.keras.layers.Dropout(rate=0.3)(hidden2)

out = tf.keras.layers.Dense(1, activation='sigmoid')(drop2)

model = tf.keras.Model(inputs=inp, outputs=out)
model.compile(optimizer='adam',
                loss='binary_crossentropy',
                metrics=['accuracy'])
model.summary()


# Output
Model: "model"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         [(None, 125, 125, 3)]     0         
_________________________________________________________________
conv2d (Conv2D)              (None, 125, 125, 32)      896       
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 62, 62, 32)        0         
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 62, 62, 64)        18496     
_________________________________________________________________
...
...
_________________________________________________________________
dense_1 (Dense)              (None, 512)               262656    
_________________________________________________________________
dropout_1 (Dropout)          (None, 512)               0         
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 513       
=================================================================
Total params: 15,102,529
Trainable params: 15,102,529
Non-trainable params: 0
_________________________________________________________________

根据此代码中的架构,我们的 CNN 模型具有三个卷积层和池化层,后跟两个密集层,以及用于正则化的 dropout。让我们训练我们的模型。

import datetime

logdir = os.path.join('/home/dipanzan_sarkar/projects/tensorboard_logs', 
                      datetime.datetime.now().strftime("%Y%m%d-%H%M%S"))
tensorboard_callback = tf.keras.callbacks.TensorBoard(logdir, histogram_freq=1)
reduce_lr = tf.keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.5,
                              patience=2, min_lr=0.000001)
callbacks = [reduce_lr, tensorboard_callback]

history = model.fit(x=train_imgs_scaled, y=train_labels_enc, 
                    batch_size=BATCH_SIZE,
                    epochs=EPOCHS, 
                    validation_data=(val_imgs_scaled, val_labels_enc), 
                    callbacks=callbacks,
                    verbose=1)
                    

# Output
Train on 17361 samples, validate on 1929 samples
Epoch 1/25
17361/17361 [====] - 32s 2ms/sample - loss: 0.4373 - accuracy: 0.7814 - val_loss: 0.1834 - val_accuracy: 0.9393
Epoch 2/25
17361/17361 [====] - 30s 2ms/sample - loss: 0.1725 - accuracy: 0.9434 - val_loss: 0.1567 - val_accuracy: 0.9513
...
...
Epoch 24/25
17361/17361 [====] - 30s 2ms/sample - loss: 0.0036 - accuracy: 0.9993 - val_loss: 0.3693 - val_accuracy: 0.9565
Epoch 25/25
17361/17361 [====] - 30s 2ms/sample - loss: 0.0034 - accuracy: 0.9994 - val_loss: 0.3699 - val_accuracy: 0.9559
                    
      

我们获得了 95.6% 的验证准确率,这非常好,尽管我们的模型看起来略微过拟合(基于查看我们的训练准确率,为 99.9%)。我们可以通过绘制训练和验证准确率以及损失曲线来清楚地了解这一点。

f, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
t = f.suptitle('Basic CNN Performance', fontsize=12)
f.subplots_adjust(top=0.85, wspace=0.3)

max_epoch = len(history.history['accuracy'])+1
epoch_list = list(range(1,max_epoch))
ax1.plot(epoch_list, history.history['accuracy'], label='Train Accuracy')
ax1.plot(epoch_list, history.history['val_accuracy'], label='Validation Accuracy')
ax1.set_xticks(np.arange(1, max_epoch, 5))
ax1.set_ylabel('Accuracy Value')
ax1.set_xlabel('Epoch')
ax1.set_title('Accuracy')
l1 = ax1.legend(loc="best")

ax2.plot(epoch_list, history.history['loss'], label='Train Loss')
ax2.plot(epoch_list, history.history['val_loss'], label='Validation Loss')
ax2.set_xticks(np.arange(1, max_epoch, 5))
ax2.set_ylabel('Loss Value')
ax2.set_xlabel('Epoch')
ax2.set_title('Loss')
l2 = ax2.legend(loc="best")

Learning curves for basic CNN

我们可以看到,在第五个 epoch 之后,总体情况似乎没有太大改善。让我们保存此模型以供将来评估。

model.save('basic_cnn.h5')

深度迁移学习

就像人类天生具有跨任务转移知识的能力一样,迁移学习使我们能够利用先前学习的任务中的知识,并将其应用于更新的、相关的任务,即使在机器学习或深度学习的背景下也是如此。如果您有兴趣深入研究迁移学习,您可以阅读我的文章《深度学习中具有实际应用的迁移学习综合实践指南》和我的书 Python 迁移学习实践

Ideas for deep transfer learning

我们想在此练习中探讨的想法是

我们是否可以利用预训练的深度学习模型(该模型在大型数据集(如 ImageNet)上训练)通过应用和转移其在我们的问题背景下的知识来解决疟疾检测问题?

我们将应用两种最流行的深度迁移学习策略。

  • 作为特征提取器的预训练模型
  • 具有微调功能的预训练模型

我们将使用牛津大学视觉几何组 (VGG) 开发的预训练 VGG-19 深度学习模型进行实验。像 VGG-19 这样的预训练模型是在一个庞大的数据集(ImageNet)上训练的,该数据集包含许多不同的图像类别。因此,该模型应该已经学习了稳健的特征层次结构,这些层次结构在 CNN 模型学习的特征方面是空间、旋转和平移不变的。因此,该模型在学习了超过一百万张图像的良好特征表示后,可以充当计算机视觉问题(如疟疾检测)的新图像的良好特征提取器。在将迁移学习的力量应用于我们的问题之前,让我们讨论一下 VGG-19 模型架构。

了解 VGG-19 模型

VGG-19 模型是一个 19 层(卷积和全连接)深度学习网络,建立在 ImageNet 数据库之上,该数据库的开发目的是用于图像识别和分类。该模型由 Karen Simonyan 和 Andrew Zisserman 构建,并在他们的论文《用于大规模图像识别的超深度卷积网络》中进行了描述。VGG-19 模型的架构是

VGG-19 Model Architecture

您可以看到,我们总共有 16 个卷积层,它们使用 3x3 卷积滤波器以及用于下采样的最大池化层和两个全连接隐藏层,每个层有 4,096 个单元,然后是一个 1,000 个单元的密集层,其中每个单元代表 ImageNet 数据库中的一个图像类别。我们不需要最后三层,因为我们将使用我们自己的全连接密集层来预测疟疾。我们更关心前五个块,以便我们可以将 VGG 模型用作有效的特征提取器。

我们将使用其中一个模型作为简单的特征提取器,方法是冻结五个卷积块,以确保它们的权重在每个 epoch 后都不会更新。对于最后一个模型,我们将对 VGG 模型应用微调,其中我们将解冻最后两个块(Block 4 和 Block 5),以便它们的权重将在每个 epoch 中更新(每个数据批次),因为我们训练我们自己的模型。

模型 2:作为特征提取器的预训练模型

为了构建此模型,我们将利用 TensorFlow 加载 VGG-19 模型并冻结卷积块,以便我们可以将它们用作图像特征提取器。我们将在最后插入我们自己的密集层来执行分类任务。

vgg = tf.keras.applications.vgg19.VGG19(include_top=False, weights='imagenet', 
                                        input_shape=INPUT_SHAPE)
vgg.trainable = False
# Freeze the layers
for layer in vgg.layers:
    layer.trainable = False
    
base_vgg = vgg
base_out = base_vgg.output
pool_out = tf.keras.layers.Flatten()(base_out)
hidden1 = tf.keras.layers.Dense(512, activation='relu')(pool_out)
drop1 = tf.keras.layers.Dropout(rate=0.3)(hidden1)
hidden2 = tf.keras.layers.Dense(512, activation='relu')(drop1)
drop2 = tf.keras.layers.Dropout(rate=0.3)(hidden2)

out = tf.keras.layers.Dense(1, activation='sigmoid')(drop2)

model = tf.keras.Model(inputs=base_vgg.input, outputs=out)
model.compile(optimizer=tf.keras.optimizers.RMSprop(lr=1e-4),
                loss='binary_crossentropy',
                metrics=['accuracy'])
model.summary()


# Output
Model: "model_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_2 (InputLayer)         [(None, 125, 125, 3)]     0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, 125, 125, 64)      1792      
_________________________________________________________________
block1_conv2 (Conv2D)        (None, 125, 125, 64)      36928     
_________________________________________________________________
...
...
_________________________________________________________________
block5_pool (MaxPooling2D)   (None, 3, 3, 512)         0         
_________________________________________________________________
flatten_1 (Flatten)          (None, 4608)              0         
_________________________________________________________________
dense_3 (Dense)              (None, 512)               2359808   
_________________________________________________________________
dropout_2 (Dropout)          (None, 512)               0         
_________________________________________________________________
dense_4 (Dense)              (None, 512)               262656    
_________________________________________________________________
dropout_3 (Dropout)          (None, 512)               0         
_________________________________________________________________
dense_5 (Dense)              (None, 1)                 513       
=================================================================
Total params: 22,647,361
Trainable params: 2,622,977
Non-trainable params: 20,024,384
_________________________________________________________________

从此输出中可以明显看出,我们的模型中有很多层,我们将仅使用 VGG-19 模型的冻结层作为特征提取器。您可以使用以下代码来验证我们的模型中有多少层确实是可训练的,以及我们的网络中存在多少层。

print("Total Layers:", len(model.layers))
print("Total trainable layers:", 
      sum([1 for l in model.layers if l.trainable]))

# Output
Total Layers: 28
Total trainable layers: 6

我们现在将使用与我们在先前模型中使用的相似的配置和回调来训练我们的模型。有关训练模型的完整代码,请参阅 我的 GitHub 存储库。我们观察到以下图表,显示模型的准确率和损失。

Learning curves for frozen pre-trained CNN

冻结的预训练 CNN 的学习曲线。

这表明我们的模型不像我们的基本 CNN 模型那样过度拟合,但性能略低于我们的基本 CNN 模型。让我们保存此模型以供将来评估。

model.save('vgg_frozen.h5')

模型 3:具有图像增强功能的微调预训练模型

在我们的最终模型中,我们将微调预训练 VGG-19 模型最后两个块中的层的权重。我们还将介绍图像增强的概念。图像增强背后的想法与名称完全一致。我们从训练数据集中加载现有图像,并对它们应用一些图像变换操作,例如旋转、剪切、平移、缩放等,以生成现有图像的新的、更改后的版本。由于这些随机变换,我们每次都不会获得相同的图像。我们将利用 tf.keras 中一个名为 ImageDataGenerator 的优秀实用程序,它可以帮助构建图像增强器。

train_datagen = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1./255,
                                                                zoom_range=0.05, 
                                                                rotation_range=25,
                                                                width_shift_range=0.05, 
                                                                height_shift_range=0.05, 
                                                                shear_range=0.05, horizontal_flip=True, 
                                                                fill_mode='nearest')

val_datagen = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1./255)

# build image augmentation generators
train_generator = train_datagen.flow(train_data, train_labels_enc, batch_size=BATCH_SIZE, shuffle=True)
val_generator = val_datagen.flow(val_data, val_labels_enc, batch_size=BATCH_SIZE, shuffle=False)

我们将不对我们的验证数据集应用任何转换(除了缩放图像,这是强制性的),因为我们将使用它来评估我们每个 epoch 的模型性能。有关迁移学习背景下的图像增强的详细说明,请随时查看我上面引用的 文章。让我们看一下一批图像增强变换的一些样本结果。

img_id = 0
sample_generator = train_datagen.flow(train_data[img_id:img_id+1], train_labels[img_id:img_id+1],
                                      batch_size=1)
sample = [next(sample_generator) for i in range(0,5)]
fig, ax = plt.subplots(1,5, figsize=(16, 6))
print('Labels:', [item[1][0] for item in sample])
l = [ax[i].imshow(sample[i][0][0]) for i in range(0,5)]

Sample augmented images

您可以清楚地看到前面输出中图像的细微变化。我们现在将构建我们的深度学习模型,确保 VGG-19 模型的最后两个块是可训练的。

vgg = tf.keras.applications.vgg19.VGG19(include_top=False, weights='imagenet', 
                                        input_shape=INPUT_SHAPE)
# Freeze the layers
vgg.trainable = True

set_trainable = False
for layer in vgg.layers:
    if layer.name in ['block5_conv1', 'block4_conv1']:
        set_trainable = True
    if set_trainable:
        layer.trainable = True
    else:
        layer.trainable = False
    
base_vgg = vgg
base_out = base_vgg.output
pool_out = tf.keras.layers.Flatten()(base_out)
hidden1 = tf.keras.layers.Dense(512, activation='relu')(pool_out)
drop1 = tf.keras.layers.Dropout(rate=0.3)(hidden1)
hidden2 = tf.keras.layers.Dense(512, activation='relu')(drop1)
drop2 = tf.keras.layers.Dropout(rate=0.3)(hidden2)

out = tf.keras.layers.Dense(1, activation='sigmoid')(drop2)

model = tf.keras.Model(inputs=base_vgg.input, outputs=out)
model.compile(optimizer=tf.keras.optimizers.RMSprop(lr=1e-5),
                loss='binary_crossentropy',
                metrics=['accuracy'])

print("Total Layers:", len(model.layers))
print("Total trainable layers:", sum([1 for l in model.layers if l.trainable]))


# Output
Total Layers: 28
Total trainable layers: 16

我们降低了模型中的学习率,因为我们不想在微调时对预训练层进行过大的权重更新。模型的训练过程将略有不同,因为我们正在使用数据生成器,因此我们将利用 fit_generator(…) 函数。

tensorboard_callback = tf.keras.callbacks.TensorBoard(logdir, histogram_freq=1)
reduce_lr = tf.keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.5,
                              patience=2, min_lr=0.000001)

callbacks = [reduce_lr, tensorboard_callback]
train_steps_per_epoch = train_generator.n // train_generator.batch_size
val_steps_per_epoch = val_generator.n // val_generator.batch_size
history = model.fit_generator(train_generator, steps_per_epoch=train_steps_per_epoch, epochs=EPOCHS,
                              validation_data=val_generator, validation_steps=val_steps_per_epoch, 
                              verbose=1)


# Output
Epoch 1/25
271/271 [====] - 133s 489ms/step - loss: 0.2267 - accuracy: 0.9117 - val_loss: 0.1414 - val_accuracy: 0.9531
Epoch 2/25
271/271 [====] - 129s 475ms/step - loss: 0.1399 - accuracy: 0.9552 - val_loss: 0.1292 - val_accuracy: 0.9589
...
...
Epoch 24/25
271/271 [====] - 128s 473ms/step - loss: 0.0815 - accuracy: 0.9727 - val_loss: 0.1466 - val_accuracy: 0.9682
Epoch 25/25
271/271 [====] - 128s 473ms/step - loss: 0.0792 - accuracy: 0.9729 - val_loss: 0.1127 - val_accuracy: 0.9641

这看起来是我们迄今为止最好的模型。它为我们提供了接近 96.5% 的验证准确率,并且根据训练准确率,我们的模型看起来不像我们的第一个模型那样过度拟合。这可以通过以下学习曲线进行验证。

Learning curves for fine-tuned pre-trained CNN

微调的预训练 CNN 的学习曲线。

让我们保存此模型,以便我们可以在测试数据集上使用它进行模型评估。

model.save('vgg_finetuned.h5')

这完成了我们的模型训练阶段。我们现在准备好在实际测试数据集上测试我们模型的性能!

深度学习模型性能评估

我们将通过使用我们在训练阶段构建的三个模型对来自我们的测试数据集的数据进行预测来评估它们,因为仅验证是不够的!我们还构建了一个名为 model_evaluation_utils 的简洁实用程序模块,我们可以使用它来评估我们的深度学习模型在相关分类指标方面的性能。第一步是缩放我们的测试数据。

test_imgs_scaled = test_data / 255.
test_imgs_scaled.shape, test_labels.shape

# Output
((8268, 125, 125, 3), (8268,))

下一步涉及加载我们保存的深度学习模型并对测试数据进行预测。

# Load Saved Deep Learning Models
basic_cnn = tf.keras.models.load_model('./basic_cnn.h5')
vgg_frz = tf.keras.models.load_model('./vgg_frozen.h5')
vgg_ft = tf.keras.models.load_model('./vgg_finetuned.h5')

# Make Predictions on Test Data
basic_cnn_preds = basic_cnn.predict(test_imgs_scaled, batch_size=512)
vgg_frz_preds = vgg_frz.predict(test_imgs_scaled, batch_size=512)
vgg_ft_preds = vgg_ft.predict(test_imgs_scaled, batch_size=512)

basic_cnn_pred_labels = le.inverse_transform([1 if pred > 0.5 else 0 
                                                  for pred in basic_cnn_preds.ravel()])
vgg_frz_pred_labels = le.inverse_transform([1 if pred > 0.5 else 0 
                                                  for pred in vgg_frz_preds.ravel()])
vgg_ft_pred_labels = le.inverse_transform([1 if pred > 0.5 else 0 
                                                  for pred in vgg_ft_preds.ravel()])

最后一步是利用我们的 model_evaluation_utils 模块并使用相关分类指标检查每个模型的性能。

import model_evaluation_utils as meu
import pandas as pd

basic_cnn_metrics = meu.get_metrics(true_labels=test_labels, predicted_labels=basic_cnn_pred_labels)
vgg_frz_metrics = meu.get_metrics(true_labels=test_labels, predicted_labels=vgg_frz_pred_labels)
vgg_ft_metrics = meu.get_metrics(true_labels=test_labels, predicted_labels=vgg_ft_pred_labels)

pd.DataFrame([basic_cnn_metrics, vgg_frz_metrics, vgg_ft_metrics], 
             index=['Basic CNN', 'VGG-19 Frozen', 'VGG-19 Fine-tuned'])

Model accuracy

看起来我们的第三个模型在测试数据集上表现最佳,模型准确率和 F1 分数均为 96%,这非常好,并且与我们前面提到的研究论文和文章中提到的更复杂的模型相当。

结论

疟疾检测并非易事,全球合格人员的可用性是病例诊断和治疗中的一个严重问题。我们研究了一个有趣的现实世界医学影像疟疾检测案例研究。易于构建的、利用人工智能的开源技术可以为我们提供最先进的疟疾检测准确率,从而使人工智能能够用于社会公益。

我鼓励您查看本文中提到的文章和研究论文,没有这些文章和研究论文,我不可能构思和撰写本文。如果您有兴趣运行或采用这些技术,本文中使用的所有代码都可以在 我的 GitHub 存储库 中找到。请记住从 官方网站 下载数据。

让我们期待在医疗保健领域更多地采用开源人工智能功能,使其对世界各地的每个人来说都更便宜、更易于访问!

User profile image.
Dipanjan (DJ) Sarkar 是红帽公司的数据科学家、已出版的作家、顾问和培训师。他曾为多家初创公司以及英特尔等财富 500 强公司提供咨询和工作。他主要致力于利用数据科学、机器学习和深度学习来构建大规模智能系统。

评论已关闭。

Creative Commons License本作品根据知识共享署名-相同方式共享 4.0 国际许可协议获得许可。
© . All rights reserved.