作者 | Dipanjan (DJ) Sarkar
译者 | Monanfei
编辑 | Rachel、Jane
出品 | AI科技大本营(id:rgznai100)
【导读】文本基于深度学习和迁移学习方法,对疟疾等传染病检测问题进行了研究。作者对疟疾的检测原理以及迁移学习理论进行了介绍,并使用VGG-19预训练模型,进行了基于特征提取和基于参数微调的迁移学习实践。
前言
"健康就是财富",这是一个老生常谈的话题,但不得不说这是一个真理。在这篇文章中,我们将研究如何利用AI技术来检测一种致命的疾病——疟疾。本文将提出一个低成本、高效率和高准确率的开源解决方案。本文有两个目的:1.了解疟疾的传染原因和其致命性;2、介绍如何运用深度学习有效检测疟疾。本章的主要内容如下:
- 开展本项目的动机
- 疟疾检测的方法
- 用深度学习检测疟疾
- 从头开始训练卷积神经网络(CNN)
- 利用预训练模型进行迁移学习
本文不是为了宣扬 AI 将要取代人类的工作,或者接管世界等论调,而是仅仅展示 AI 是如何用一种低成本、高效率和高准确率的方案,来帮助人类去检测和诊断疟疾,并尽量减少人工操作。
Python and TensorFlow — A great combo to build open-source deep learning solutions
在本文中,我们将使用 Python 和 tensorflow ,来构建一个强大的、可扩展的、有效的深度学习解决方案。这些工具都是免费并且开源的,这使得我们能够构建一个真正低成本、高效精准的解决方案,而且可以让每个人都可以轻松使用。让我们开始吧!
动机
疟疾是经疟蚊叮咬而感染疟原虫所引起的虫媒传染病,疟疾最常通过受感染的雌性疟蚊来传播。虽然我们不必详细了解这种疾病,但是我们需要知道疟疾有五种常见的类型。下图展示了这种疾病的致死性在全球的分布情况。
Malaria Estimated Risk Heath Map (Source: treated.com)
从上图中可以明显看到,疟疾遍布全球,尤其是在热带区域分布密集。本项目就是基于这种疾病的特性和致命性来开展的,下面我们举个例子来说明。起初,如果你被一只受感染的蚊子叮咬了,那么蚊子所携带的寄生虫就会进入你的血液,并且开始摧毁你体内的携氧红细胞。通常来讲,你会在被疟蚊叮咬后的几天或几周内感到不适,一般会首先出现类似流感或者病毒感染的症状。然而,这些致命的寄生虫可以在你身体里完好地存活超过一年的时间,并且不产生任何其他症状!延迟接受正确的治疗,可能会导致并发症甚至死亡。因此,早期并有效的疟疾检测和排查可以挽救这些生命。
世界卫生组织(WHO)发布了几个关于疟疾的重要事实,详情见此。简而言之,世界上将近一半的人口面临疟疾风险,每年有超过2亿的疟疾病例,以及有大约40万人死于疟疾。这些事实让我们认识到,快速简单高效的疟疾检查是多么重要,这也是本文的动机所在。
疟疾检查的方法
文章《 Pre-trained convolutional neural networks as feature extractors toward improved Malaria parasite detection in thin blood smear images》(本文的数据和分析也是基于这篇文章)简要介绍了疟疾检测的几种方法,这些方法包括但是不限于厚薄血涂片检查、聚合酶链式反应(PCR)和快速诊断测试(RDT)。在本文中,我们没有对这些方法进行详细介绍,但是需要注意的一点是,后两种方法常常作为替代方案使用,尤其是在缺乏高质量显微镜服务的情况下。
我们将简要讨论基于血液涂片检测流程的标准疟疾诊断方法,首先感谢 Carlos Ariza 的博文,以及 Adrian Rosebrock 关于疟疾检查的文章,这两篇文章让我们对疟疾检查领域有了更为深入的了解。
A blood smear workflow for Malaria detection (Source)
根据上图所示的 WHO 的血液涂片检测流程,该工作包括在100倍放大倍数下对血涂片进行深入检查,其中人们需要从5000个细胞中,手动检测出含有寄生虫的红细胞。Rajaraman 等人的论文中更加详细的给出了相关的描述,如下所示:
厚血涂片有助于检测寄生虫的存在,而薄血涂片有助于识别引起感染的寄生虫种类(Centers for Disease Control and Prevention, 2012)。诊断准确性在很大程度上取决于人类的专业知识,并且可能受到观察者间的差异和观察者的可靠性所带来的不利影响,以及受到在疾病流行或资源受限的区域内的大规模诊断造成的负担所带来的不利影响(Mitiku,Mengistu&Gelaw,2003)。替代技术,例如聚合酶链式反应(PCR)和快速诊断测试(RDT),也会被使用;但是PCR分析受到其性能的限制(Hommelsheim等,2014),而RDT在疾病流行地区的成本效益较低(Hawkes,Katsuva&Masumbuko,2009)。
因此,传统的疟疾检测绝对是一个密集的手工过程,或许深度学习技术可以帮助它完成自动化。上文提到的这些内容为后文打下了基础。
用深度学习检测疟疾
手工诊断血液涂片,是一项重复且规律的工作,而且需要一定的专业知识来区分和统计被寄生的和未感染的细胞。如果某些地区的工作人员没有正确的专业知识,那么这种方法就不能很好地推广,并且会导致一些问题。现有工作已经取得了一些进展,包括利用最先进的图像处理和分析技术来提取手工设计的特征,并利用这些特性构建基于机器学习的分类模型。但是,由于手工设计的部分需要花费大量的时间,当有更多的数据可供训练时,模型却无法及时的进行扩展。
深度学习模型,或更具体地说,卷积神经网络(CNN)在各种计算机视觉任务中获得了非常好的效果。本文假设您已经对 CNN 有一定的了解,但是如果您并不了解 CNN ,可以通过这篇文章进行深入了解。简单来讲,CNN 最关键的层主要包括卷积层和池化层,如下图所示。
A typical CNN architeture (Source: deeplearning.net)
卷积层从数据中学习空间层级模式,这些模式具有平移不变性,因此卷积层能够学习图像的不同方面。例如,第一卷积层将学习诸如边缘和角落的微型局部模式,第二卷积层将基于第一层所提取的特征,来学习更大的图像模式,如此循序渐进。这使得 CNN 能够自动进行特征工程,并且学习有效的特征,这些特征对新的数据具有很好的泛化能力。池化层常用于下采样和降维。
因此,CNN 能够帮助我们实现自动化的和可扩展的特征工程。此外,在模型的末端接入密集层,能够使我们执行图像分类等任务。使用像CNN这样的深度学习模型,进行自动化的疟疾检测,可能是一个高效、低成本、可扩展的方案。特别是随着迁移学习的发展和预训练模型的共享,在数据量较少等限制条件下,深度学习模型也能取得很好的效果。
Rajaraman 等人的论文 《Pre-trained convolutional neural networks as feature extractors toward improved parasite detection in thin blood smear images》利用 6 个预训练模型,在进行疟疾检测时取得了 95.9% 的准确率。本文的重点是从头开始尝试一些简单的 CNN 模型和一些预先训练的模型,并利用迁移学习来检验我们在同一数据集下得到的结果。本文将使用 Python 和 TensorFlow 框架来构建模型。
数据集的详情
首先感谢 Lister Hill 国家生物医学通信中心(LHNCBC)的研究人员(国家医学图书馆(NLM)的部门),他们仔细收集并注释了这个血涂片图像的数据集,数据中包含健康和感染这两种类型的血涂片图像。您可以从官方网站上下载这些图像。
实际上,他们开发了一款可以运行在标准安卓智能手机上的应用程序,该程序可以连接传统的光学显微镜 (Poostchi et al., 2018) 。他们从孟加拉国吉大港医学院附属医院进行拍照记录了样本集,其中包括150个恶性疟原虫感染的样本和 50 个健康的样本,每个样本都是经过 Giemsa 染色的薄血涂片。智能手机的内置摄像头可以捕获样本的每一个局部微观视图。来自泰国曼谷的玛希隆-牛津热带医学研究所的专业人员为这些图像进行了手动注释。让我们简要地看一下数据集结构。首先根据本文所使用的操作系统,我们需要安装一些基本的依赖项。
本文所使用的系统是云上的 Debian 系统,该系统配置有 GPU ,这能够加速我们模型的训练。首先安装依赖树,这能够方便我们查看目录结构。(sudo apt install tree)
从上图所示的目录结构中可以看到,我们的文件里包含两个文件夹,分别包含受感染的和健康的细胞图像。利用以下代码,我们可以进一步了解图像的总数是多少。
代码语言:javascript复制import osimport 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)
从上述结果可以看到, 疟疾和非疟疾(未感染)的细胞图像的数据集均包含13779张图片,两个数据集的大小是相对平衡的。接下来我们将利用这些数据构建一个基于pandas的dataframe类型的数据,这对我们后续构建数据集很有帮助。
代码语言:javascript复制import numpy as npimport 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()
构建和探索图像数据集
在构建深度学习模型之前,我们不仅需要训练数据,还需要未用于训练的数据来验证和测试模型的性能。本文采用 60:10:30 的比例来划分训练集、验证集和测试集。我们将使用训练集和验证集来训练模型,并利用测试集来检验模型的性能。
代码语言:javascript复制from sklearn.model_selection import train_test_splitfrom 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})
可以发现,由于血液来源、测试方法以及图像拍摄的方向不同,血液涂片和细胞的图像尺寸不尽相同。我们需要获取一些训练数据的统计信息,从而确定最优的图像尺寸(请注意,在这里我们完全没用到测试集!)。
代码语言:javascript复制import cv2from concurrent import futuresimport 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))
# OutputStarting Img shape computation:ThreadPoolExecutor-0_0: working on img num: 0ThreadPoolExecutor-0_17: working on img num: 5000ThreadPoolExecutor-0_15: working on img num: 10000ThreadPoolExecutor-0_1: working on img num: 15000ThreadPoolExecutor-0_7: working on img num: 17360Min Dimensions: [46 46 3]Avg Dimensions: [132.77311215 132.45757733 3.]Median Dimensions: [130. 130. 3.]Max Dimensions: [385 394 3]
我们采用了并行处理的策略来加速图像读取操作。基于汇总的统计信息,我们决定将每张图像的大小调整为125x125。现在让我们加载所有的图像,并把他们的大小都调整为上述固定的尺寸。
代码语言:javascript复制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
# OutputLoading Train Images:ThreadPoolExecutor-1_0: working on img num: 0ThreadPoolExecutor-1_12: working on img num: 5000ThreadPoolExecutor-1_6: working on img num: 10000ThreadPoolExecutor-1_10: working on img num: 15000ThreadPoolExecutor-1_3: working on img num: 17360
Loading Validation Images:ThreadPoolExecutor-1_13: working on img num: 0ThreadPoolExecutor-1_18: working on img num: 1928
Loading Test Images:ThreadPoolExecutor-1_5: working on img num: 0ThreadPoolExecutor-1_19: working on img num: 5000ThreadPoolExecutor-1_8: working on img num: 8267((17361, 125, 125, 3), (1929, 125, 125, 3), (8268, 125, 125, 3))
我们再次运用了并行处理策略来加速图像加载和尺寸调整的计算,如上面输出结果中展示的,我们最终得到了所需尺寸的图像张量。现在我们可以查看一些样本的细胞图像,从而从直观上认识一下我们的数据的情况。
代码语言:javascript复制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([])
从上面的样本图像可以看出,疟疾和健康细胞图像之间存在一些细微差别。我们将构建深度学习模型,通过不断训练来使模型尝试学习这些模式。在开始训练模型之前,我们先对模型的参数进行一些基本的设置。
代码语言:javascript复制BATCH_SIZE = 64NUM_CLASSES = 2EPOCHS = 25INPUT_SHAPE = (125, 125, 3)
train_imgs_scaled = train_data / 255.val_imgs_scaled = val_data / 255.
# encode text category labelsfrom 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月发布,它为我们项目的实施提供了一个完美的接口。
代码语言:javascript复制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)。首先,我们需要定义模型的结构。
代码语言:javascript复制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()
# OutputModel: "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,529Trainable params: 15,102,529Non-trainable params: 0_________________________________________________________________
上述代码所构建的 CNN 模型,包含3个卷积层、1个池化层以及2个全连接层,并对全连接层设置 dropout 参数用于正则化。现在让我们开始训练模型吧!
代码语言:javascript复制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)
# OutputTrain on 17361 samples, validate on 1929 samplesEpoch 1/2517361/17361 [====] - 32s 2ms/sample - loss: 0.4373 - accuracy: 0.7814 - val_loss: 0.1834 - val_accuracy: 0.9393Epoch 2/2517361/17361 [====] - 30s 2ms/sample - loss: 0.1725 - accuracy: 0.9434 - val_loss: 0.1567 - val_accuracy: 0.9513......Epoch 24/2517361/17361 [====] - 30s 2ms/sample - loss: 0.0036 - accuracy: 0.9993 - val_loss: 0.3693 - val_accuracy: 0.9565Epoch 25/2517361/17361 [====] - 30s 2ms/sample - loss: 0.0034 - accuracy: 0.9994 - val_loss: 0.3699 - val_accuracy: 0.9559
从上面的结果可以看到,我们的模型在验证集上的准确率为 95.6% ,这是非常好的。我们注意到模型在训练集上的准确率为 99.9% ,这看起来有一些过拟合。为了更加清晰地查看这个问题,我们可以分别绘制在训练和验证阶段的准确度曲线和损失曲线。
代码语言:javascript复制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']) 1epoch_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
从图中可以看出,在第5个 epoch 之后,在验证集上的精度似乎不再提高。我们先将这个模型保存,在后面我们会再次用到它。
代码语言:javascript复制model.save('basic_cnn.h5')
深度迁移学习
就像人类能够运用知识完成跨任务工作一样,迁移学习使得我们能够利用在先前任务中学习到的知识,来处理新的任务,在机器学习和深度学习的环境下也是如此。这些文章涵盖了迁移学习的详细介绍和讨论,有兴趣的读者可以参考学习。
Ideas for deep transfer learning
我们能否采用迁移学习的思想,将预训练的深度学习模型(已在大型数据集上进行过训练的模型——例如 ImageNet)的知识应用到我们的问题——进行疟疾检测上呢?我们将采用两种目前最主流的迁移学习策略。
- 将预训练模型作为特征提取器
- 对预训练模型进行微调
我们将使用由牛津大学视觉几何组(VGG)所开发的预训练模型 VGG-19 进行实验。像 VGG-19 这样的预训练模型,一般已经在大型数据集上进行过训练,这些数据集涵盖多种类别的图像。基于此,这些预训练模型应该已经使用CNN模型学习到了一个具有高度鲁棒性的特征的层次结构,并且其应具有尺度、旋转和平移不变性。因此,这个已经学习了超过一百万个图像的具有良好特征表示的模型,可以作为一个很棒的图像特征提取器,为包括疟疾检测问题在内的其他计算机视觉问题服务。在引入强大的迁移学习之前,我们先简要讨论一下 VGG-19 的结构。
理解VGG-19模型
VGG-19 是一个具有 19 个层(包括卷积层和全连接层)的深度学习网络,该模型基于 ImageNet 数据集进行训练,该数据集是专门为图像识别和分类所构建的。VGG-19 是由 Karen Simonyan 和 Andrew Zisserman 提出的,该模型在他们的论文《Very Deep Convolutional Networks for Large-Scale Image Recognition》中有详细介绍,建议有兴趣的读者可以去读一读这篇优秀的论文。VGG-19 模型的结构如下图所示。
VGG-19 Model Architecture
从上图可以清楚地看到,该模型具有 16 个使用 3x3 卷积核的卷积层,其中部分卷积层后面接了一个最大池化层,用于下采样;随后依次连接了两个具有 4096 个隐层神经元的全连接层,接着连接了一个具有 1000 个隐层神经元的全连接层, 最后一个全连接层的每个神经元都代表 ImageNet 数据集中的一个图像类别。由于我们需要使用新的全连接层来分类疟疾,因此我们不需要最后的三个全连接层。我们更关心的是前五个块,以便我们可以利用 VGG 模型作为有效的特征提取器。
前文提到有两种迁移学习的策略,对于第一种策略,我们将把 VGG 模型当做一个特征提取器,这可以通过冻结前五个卷积块,使得它们的权重参数不会随着新的训练过程而更新来实现。对于第二种策略,我们将会解冻最后的两个卷积块(模块4和模块5),从而使得它们的参数会随着新的训练过程而不断更新。
模型2:将预训练模型作为特征提取机
为了构建这个模型,我们将利用 TensorFlow 加载 VGG-19 模型,并冻结它的卷积块,以便我们可以将其用作图像特征提取器。我们将在该模型的末尾插入自己的全连接层,用于执行本文的分类任务。
代码语言:javascript复制vgg = tf.keras.applications.vgg19.VGG19(include_top=False, weights='imagenet',input_shape=INPUT_SHAPE)vgg.trainable = False# Freeze the layersfor layer in vgg.layers: layer.trainable = False base_vgg = vggbase_out = base_vgg.outputpool_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()
# OutputModel: "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,361Trainable params: 2,622,977Non-trainable params: 20,024,384
从上面代码的输出可以看到,我们的模型有很多层,并且我们仅仅只利用了 VGG-19 的冻结层来提取特征。下面的代码可以验证本模型中有多少层用于训练,以及检验本模型中一共有多少层。
代码语言:javascript复制print("Total Layers:", len(model.layers))print("Total trainable layers:",sum([1 for l in model.layers if l.trainable]))
# OutputTotal Layers: 28Total trainable layers: 6
现在我们将训练该模型,在训练过程中所用到的配置和回调函数与模型1中的类似,完整的代码可以参考github链接。下图展示了在训练过程中,模型的准确度曲线和损失曲线。
Learning Curves for frozen pre-trained CNN
从上图可以看出,该模型不像模型1中基本的 CNN 模型那样存在过拟合的现象,但是性能并不是很好。事实上,它的性能还没有基本的 CNN 模型好。现在我们将模型保存,用于后续的评估。
代码语言:javascript复制model.save( 'vgg_frozen.h5')
模型3:具有图像增广的微调的预训练模型
在这个模型中,我们将微调预训练 VGG-19 模型的最后两个区块中层的权重。除此之外,我们还将介绍图像增广的概念。图像增广背后的原理与它的名称听起来完全一样。我们首先从训练数据集中加载现有的图像,然后对它们进行一些图像变换的操作,例如旋转,剪切,平移,缩放等,从而生成现有图像的新的、变化的版本。由于这些随机变换的操作,我们每次都会得到不同的图像。我们将使用 tf.keras 中的 ImageDataGenerator 工具,它能够帮助我们实现图像增广。
代码语言:javascript复制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 generatorstrain_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 结束后,用验证集来评估我们的模型。有关图像增广的详细说明,可以参考这篇文章。让我们来看看进行图像增广变换后的一些样本结果。
代码语言:javascript复制img_id = 0sample_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 模型的最后两个块可以进行训练。
代码语言:javascript复制vgg = tf.keras.applications.vgg19.VGG19(include_top=False, weights='imagenet',input_shape=INPUT_SHAPE)# Freeze the layersvgg.trainable = True
set_trainable = Falsefor 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 = vggbase_out = base_vgg.outputpool_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]))
# OutputTotal Layers: 28Total trainable layers: 16
由于我们不希望在微调过程中,对预训练的层进行较大的权重更新,我们降低了模型的学习率。由于我们使用数据生成器来加载数据,本模型的训练过程会和之前稍稍不同,在这里,我们需要用到函数 fit_generator(…) 。
代码语言:javascript复制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_sizeval_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)
# OutputEpoch 1/25271/271 [====] - 133s 489ms/step - loss: 0.2267 - accuracy: 0.9117 - val_loss: 0.1414 - val_accuracy: 0.9531Epoch 2/25271/271 [====] - 129s 475ms/step - loss: 0.1399 - accuracy: 0.9552 - val_loss: 0.1292 - val_accuracy: 0.9589......Epoch 24/25271/271 [====] - 128s 473ms/step - loss: 0.0815 - accuracy: 0.9727 - val_loss: 0.1466 - val_accuracy: 0.9682Epoch 25/25271/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
现在让我们保存这个模型,很快我们将在测试集上用到它进行性能评估。
代码语言:javascript复制model.save( 'vgg_finetuned.h5')
至此,模型训练阶段告一段落,我们即将在真实的测试集上去测试这些模型的性能。
深度学习模型的性能评估阶段
现在,我们将对之前训练好的三个模型进行评估。仅仅使用验证集来评估模型的好坏是不够的, 因此,我们将使用测试集来进一步评估模型的性能。我们构建了一个实用的模块 model_evaluation_utils,该模块采用相关的分类指标,用于评估深度学习模型的性能。首先我们需要将测试数据进行缩放。
代码语言:javascript复制test_imgs_scaled = test_data / 255.test_imgs_scaled.shape, test_labels.shape
# Output((8268, 125, 125, 3), (8268,))
第二步是加载之前所保存的深度学习模型,然后在测试集上进行预测。
代码语言:javascript复制# Load Saved Deep Learning Modelsbasic_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 Databasic_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 模块,根据不同的分类评价指标,来评估每个模型的性能。
代码语言:javascript复制import model_evaluation_utils as meuimport 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'])
从图中可以看到,第三个模型在测试集上的性能是最好的,其准确度和 f1-score 都达到了96%,这是一个非常好的结果,而且这个结果和论文中提到的更为复杂的模型所得到的结果具有相当的 可比性!
结论
本文研究了一个有趣的医学影像案例——疟疾检测。疟疾检测是一个复杂的过程,而且能够进行正确操作的医疗人员也很少,这是一个很严重的问题。本文利用 AI 技术构建了一个开源的项目,该项目在疟疾检测问题上具有最高的准确率,并使AI技术为社会带来了效益。
相关链接:https://towardsdatascience.com/detecting-malaria-with-deep-learning-9e45c1e34b60