使用深度學習檢測瘧疾

2019-05-24 02:00:00

人工智慧結合開源硬體工具能夠提升嚴重傳染病瘧疾的診斷。

人工智慧(AI)和開源工具、技術和框架是促進社會進步的強有力的結合。“健康就是財富”可能有點陳詞濫調,但它卻是非常準確的!在本篇文章,我們將測試 AI 是如何與低成本、有效、精確的開源深度學習方法結合起來一起用來檢測致死的傳染病瘧疾。

我既不是一個醫生,也不是一個醫療保健研究者,我也絕不像他們那樣合格,我只是對將 AI 應用到醫療保健研究感興趣。在這片文章中我的想法是展示 AI 和開源解決方案如何幫助瘧疾檢測和減少人工勞動的方法。

Python and TensorFlow

Python 和 TensorFlow: 一個構建開源深度學習方法的很棒的結合

感謝 Python 的強大和像 TensorFlow 這樣的深度學習框架,我們能夠構建健壯的、大規模的、有效的深度學習方法。因為這些工具是自由和開源的,我們能夠構建非常經濟且易於被任何人採納和使用的解決方案。讓我們開始吧!

專案動機

瘧疾是由瘧原蟲造成的致死的、有傳染性的、蚊子傳播的疾病,主要通過受感染的雌性按蚊叮咬傳播。共有五種寄生蟲能夠引起瘧疾,但是大多數病例是這兩種型別造成的:惡性瘧原蟲和間日瘧原蟲。

瘧疾熱圖

這個地圖顯示了瘧疾在全球傳播分布形勢,尤其在熱帶地區,但疾病的性質和致命性是該專案的主要動機。

如果一隻受感染雌性蚊子叮咬了你,蚊子攜帶的寄生蟲進入你的血液,並且開始破壞攜帶氧氣的紅細胞(RBC)。通常,瘧疾的最初症狀類似於流感病毒,在蚊子叮咬後,他們通常在幾天或幾週內發作。然而,這些致死的寄生蟲可以在你的身體裡生存長達一年並且不會造成任何症狀,延遲治療可能造成並行症甚至死亡。因此,早期的檢查能夠挽救生命。

世界健康組織(WHO)的瘧疾實情表明,世界近乎一半的人口面臨瘧疾的風險,有超過 2 億的瘧疾病例,每年由於瘧疾造成的死亡將近 40 萬。這是使瘧疾檢測和診斷快速、簡單和有效的一個動機。

檢測瘧疾的方法

有幾種方法能夠用來檢測和診斷瘧疾。該文中的專案就是基於 Rajaraman, et al. 的論文:“預先訓練的折積神經網路作為特徵提取器,用於改善薄血塗片影象中的瘧疾寄生蟲檢測”介紹的一些方法,包含聚合酶鏈反應(PCR)和快速診斷測試(RDT)。這兩種測試通常用於無法提供高品質顯微鏡服務的地方。

標準的瘧疾診斷通常是基於血液塗片工作流程的,根據 Carlos Ariza 的文章“Malaria Hero:一個更快診斷瘧原蟲的網路應用”,我從中了解到 Adrian Rosebrock 的“使用 Keras 的深度學習和醫學影象分析”。我感激這些優秀的資源的作者,讓我在瘧原蟲預防、診斷和治療方面有了更多的想法。

瘧原蟲檢測的血塗片工作流程

一個瘧原蟲檢測的血塗片工作流程

根據 WHO 方案,診斷通常包括對放大 100 倍的血塗片的集中檢測。受過訓練的人們手工計算在 5000 個細胞中有多少紅細胞中包含瘧原蟲。正如上述解釋中參照的 Rajaraman, et al. 的論文:

厚血塗片有助於檢測寄生蟲的存在,而薄血塗片有助於識別引起感染的寄生蟲種類(疾病控制和預防中心, 2012)。診斷準確性在很大程度上取決於診斷人的專業知識,並且可能受到觀察者間差異和疾病流行/資源受限區域大規模診斷所造成的不利影響(Mitiku, Mengistu 和 Gelaw, 2003)。可替代的技術是使用聚合酶鏈反應(PCR)和快速診斷測試(RDT);然而,PCR 分析受限於它的效能(Hommelsheim, et al., 2014),RDT 在疾病流行的地區成本效益低(Hawkes, Katsuva 和 Masumbuko, 2009)。

因此,瘧疾檢測可能受益於使用機器學習的自動化。

瘧疾檢測的深度學習

人工診斷血塗片是一個繁重的手工過程,需要專業知識來分類和計數被寄生蟲感染的和未感染的細胞。這個過程可能不能很好的規模化,尤其在那些專業人士不足的地區。在利用最先進的影象處理和分析技術提取人工選取特徵和構建基於機器學習的分類模型方面取得了一些進展。然而,這些模型不能大規模推廣,因為沒有更多的資料用來訓練,並且人工選取特徵需要花費很長時間。

深度學習模型,或者更具體地講,折積神經網路(CNN),已經被證明在各種計算機視覺任務中非常有效。(如果你想更多的了解關於 CNN 的背景知識,我推薦你閱讀視覺識別的 CS2331n 折積神經網路。)簡單地講,CNN 模型的關鍵層包含折積和池化層,正如下圖所示。

A typical CNN architecture

一個典型的 CNN 架構

折積層從資料中學習空間層級模式,它是平移不變的,因此它們能夠學習影象的不同方面。例如,第一個折積層將學習小的和區域性圖案,例如邊緣和角落,第二個折積層將基於第一層的特徵學習更大的圖案,等等。這允許 CNN 自動化提取特徵並且學習對於新資料點通用的有效的特徵。池化層有助於下取樣和減少尺寸。

因此,CNN 有助於自動化和規模化的特徵工程。同樣,在模型末尾加上密集層允許我們執行像影象分類這樣的任務。使用像 CNN 這樣的深度學習模型自動的瘧疾檢測可能非常有效、便宜和具有規模性,尤其是遷移學習和預訓練模型效果非常好,甚至在少量資料的約束下。

Rajaraman, et al. 的論文在一個資料集上利用六個預訓練模型在檢測瘧疾對比無感染樣本獲取到令人吃驚的 95.9% 的準確率。我們的重點是從頭開始嘗試一些簡單的 CNN 模型和用一個預訓練的訓練模型使用遷移學習來檢視我們能夠從相同的資料集中得到什麼。我們將使用開源工具和框架,包括 Python 和 TensorFlow,來構建我們的模型。

資料集

我們分析的資料來自 Lister Hill 國家生物醫學交流中心(LHNCBC)的研究人員,該中心是國家醫學圖書館(NLM)的一部分,他們細心收集和標記了公開可用的健康和受感染的血塗片影象的資料集。這些研究者已經開發了一個執行在 Android 智慧手機的瘧疾檢測手機應用,連線到一個傳統的光學顯微鏡。它們使用吉姆薩染液將 150 個受惡性瘧原蟲感染的和 50 個健康病人的薄血塗片染色,這些薄血塗片是在孟加拉的吉大港醫學院附屬醫院收集和照相的。使用智慧手機的內建相機獲取每個顯微鏡視窗內的影象。這些圖片由在泰國曼谷的馬希多-牛津熱帶醫學研究所的一個專家使用幻燈片閱讀器標記的。

讓我們簡要地檢視一下資料集的結構。首先,我將安裝一些基礎的依賴(基於使用的作業系統)。

Installing dependencies

我使用的是雲上的帶有一個 GPU 的基於 Debian 的作業系統,這樣我能更快的執行我的模型。為了檢視目錄結構,我們必須使用 sudo apt install tree 安裝 tree 及其依賴(如果我們沒有安裝的話)。

Installing the tree dependency

我們有兩個資料夾包含血細胞的影象,包括受感染的和健康的。我們通過輸入可以獲取關於影象總數更多的細節:

import osimport globbase_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 npimport pandas as pdnp.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_splitfrom collections import Countertrain_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 cv2from concurrent import futuresimport threadingdef 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 畫素。讓我們載入我們所有的影象並重新調整它們為這些固定尺寸。

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 imgex = 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: 17360Loading Validation Images:ThreadPoolExecutor-1_13: working on img num: 0ThreadPoolExecutor-1_18: working on img num: 1928Loading 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))

我們再次應用並行處理來加速有關影象載入和重新調整大小的計算。最終,我們獲得了所需尺寸的圖片張量,正如前面的輸出所示。我們現在檢視一些血細胞影象樣本,以對我們的資料有個印象。

import matplotlib.pyplot as plt%matplotlib inlineplt.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 = 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 LabelEncoderle = 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]

我們修復我們的影象尺寸、批次大小,和紀元,並編碼我們的分類的類標籤。TensorFlow 2.0 於 2019 年三月發布,這個練習是嘗試它的完美理由。

import tensorflow as tf# Load the TensorBoard notebook extension (optional)%load_ext tensorboard.notebooktf.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()# 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 模型有三個折積和一個池化層,其後是兩個緻密層,以及用於正則化的失活。讓我們訓練我們的模型。

import datetimelogdir = 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%)。通過繪製訓練和驗證的精度和損失曲線,我們可以清楚地看到這一點。

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

基礎 CNN 學習曲線

我們可以看在在第五個紀元,情況並沒有改善很多。讓我們儲存這個模型用於將來的評估。

model.save('basic_cnn.h5')

深度遷移學習

就像人類有與生俱來在不同任務間傳輸知識的能力一樣,遷移學習允許我們利用從以前任務學到的知識用到新的相關的任務,即使在機器學習或深度學習的情況下也是如此。如果想深入探究遷移學習,你應該看我的文章“一個易於理解與現實應用一起學習深度學習中的遷移學習的指導實踐”和我的書《Python 遷移學習實踐》。

深度遷移學習的想法

在這篇實踐中我們想要探索的想法是:

在我們的問題背景下,我們能夠利用一個預訓練深度學習模型(在巨量資料集上訓練的,像 ImageNet)通過應用和遷移知識來解決瘧疾檢測的問題嗎?

我們將應用兩個最流行的深度遷移學習策略。

  • 預訓練模型作為特徵提取器
  • 微調的預訓練模型

我們將使用預訓練的 VGG-19 深度訓練模型(由劍橋大學的視覺幾何組(VGG)開發)進行我們的實驗。像 VGG-19 這樣的預訓練模型是在一個大的資料集(Imagenet)上使用了很多不同的影象分類訓練的。因此,這個模型應該已經學習到了健壯的特徵層級結構,相對於你的 CNN 模型學到的特徵,是空間不變的、轉動不變的、平移不變的。因此,這個模型,已經從百萬幅圖片中學習到了一個好的特徵顯示,對於像瘧疾檢測這樣的計算機視覺問題,可以作為一個好的合適新影象的特徵提取器。在我們的問題中發揮遷移學習的能力之前,讓我們先討論 VGG-19 模型。

理解 VGG-19 模型

VGG-19 模型是一個構建在 ImageNet 資料庫之上的 19 層(折積和全連線的)的深度學習網路,ImageNet 資料庫為了影象識別和分類的目的而開發。該模型是由 Karen Simonyan 和 Andrew Zisserman 構建的,在他們的論文“大規模影象識別的非常深的折積網路”中進行了描述。VGG-19 的架構模型是:

VGG-19 模型架構

你可以看到我們總共有 16 個使用 3x3 折積過濾器的折積層,與最大的池化層來下取樣,和由 4096 個單元組成的兩個全連線的隱藏層,每個隱藏層之後跟隨一個由 1000 個單元組成的緻密層,每個單元代表 ImageNet 資料庫中的一個分類。我們不需要最後三層,因為我們將使用我們自己的全連線緻密層來預測瘧疾。我們更關心前五個塊,因此我們可以利用 VGG 模型作為一個有效的特徵提取器。

我們將使用模型之一作為一個簡單的特徵提取器,通過凍結五個折積塊的方式來確保它們的位權在每個紀元後不會更新。對於最後一個模型,我們會對 VGG 模型進行微調,我們會解凍最後兩個塊(第 4 和第 5)因此當我們訓練我們的模型時,它們的位權在每個時期(每批資料)被更新。

模型 2:預訓練的模型作為一個特徵提取器

為了構建這個模型,我們將利用 TensorFlow 載入 VGG-19 模型並凍結折積塊,因此我們能夠將它們用作特徵提取器。我們在末尾插入我們自己的緻密層來執行分類任務。

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 模型的凍結層作為特徵提取器。你可以使用下列程式碼來驗證我們的模型有多少層是實際可訓練的,以及我們的網路中總共存在多少層。

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

我們將使用和我們之前的模型相似的設定和回撥來訓練我們的模型。參考我的 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 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)

我們不會對我們的驗證資料集應用任何轉換(除非是調整大小,因為這是必須的),因為我們將使用它評估每個紀元的模型效能。對於在傳輸學習環境中的影象增強的詳細解釋,請隨時檢視我上面參照的文章。讓我們從一批影象增強轉換中檢視一些樣本結果。

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 模型的最後兩塊是可以訓練的。

vgg = tf.keras.applications.vgg19.VGG19(include_top=False, weights='imagenet',                                         input_shape=INPUT_SHAPE)# Freeze the layersvgg.trainable = Trueset_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(...) 函數。

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_sizehistory = 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

微調過的預訓練 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 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 模組根據相應分類指標來檢查每個模組的效能。

import model_evaluation_utils as meuimport pandas as pdbasic_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

看起來我們的第三個模型在我們的測試集上執行的最好,給出了一個模型精確性為 96% 的 F1 得分,這非常好,與我們之前提到的研究論文和文章中的更複雜的模型相當。

總結

瘧疾檢測不是一個簡單的過程,全球的合格人員的不足在病例診斷和治療當中是一個嚴重的問題。我們研究了一個關於瘧疾的有趣的真實世界的醫學影像案例。利用 AI 的、易於構建的、開源的技術在檢測瘧疾方面可以為我們提供最先進的精確性,因此使 AI 具有社會效益。

我鼓勵你檢視這篇文章中提到的文章和研究論文,沒有它們,我就不能形成概念並寫出來。如果你對執行和採納這些技術感興趣,本篇文章所有的程式碼都可以在我的 GitHub 倉庫獲得。記得從官方網站下載資料。

讓我們希望在健康醫療方面更多的採納開源的 AI 能力,使它在世界範圍內變得更便宜、更易用。