醫生再添新助手!深度學習診斷傳染病

作者 | 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.

從上圖中可以明顯看到,瘧疾遍佈全球,尤其是在熱帶區域分佈密集。本項目就是基於這種疾病的特性和致命性來開展的,下面我們舉個例子來說明。起初,如果你被一隻受感染的蚊子叮咬了,那麼蚊子所攜帶的寄生蟲就會進入你的血液,並且開始摧毀你體內的攜氧紅細胞。通常來講,你會在被瘧蚊叮咬後的幾天或幾周內感到不適,一般會首先出現類似流感或者病毒感染的症狀。然而,這些致命的寄生蟲可以在你身體裡完好地存活超過一年的時間,並且不產生任何其他症狀!延遲接受正確的治療,可能會導致併發症甚至死亡。因此,早期並有效的瘧疾檢測和排查可以挽救這些生命。

世界衛生組織(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 (Sour

根據上圖所示的 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)

醫生再添新助手!深度學習診斷傳染病 | 完整代碼+實操

從上圖所示的目錄結構中可以看到,我們的文件裡包含兩個文件夾,分別包含受感染的和健康的細胞圖像。利用以下代碼,我們可以進一步瞭解圖像的總數是多少。

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)

從上述結果可以看到, 瘧疾和非瘧疾(未感染)的細胞圖像的數據集均包含13779張圖片,兩個數據集的大小是相對平衡的。接下來我們將利用這些數據構建一個基於pandas的dataframe類型的數據,這對我們後續構建數據集很有幫助。

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()


醫生再添新助手!深度學習診斷傳染病 | 完整代碼+實操

構建和探索圖像數據集

在構建深度學習模型之前,我們不僅需要訓練數據,還需要未用於訓練的數據來驗證和測試模型的性能。本文采用 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([])


醫生再添新助手!深度學習診斷傳染病 | 完整代碼+實操

從上面的樣本圖像可以看出,瘧疾和健康細胞圖像之間存在一些細微差別。我們將構建深度學習模型,通過不斷訓練來使模型嘗試學習這些模式。在開始訓練模型之前,我們先對模型的參數進行一些基本的設置。

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 模型,包含3個卷積層、1個池化層以及2個全連接層,並對全連接層設置 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


從圖中可以看出,在第5個 epoch 之後,在驗證集上的精度似乎不再提高。我們先將這個模型保存,在後面我們會再次用到它。

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 模型,並凍結它的卷積塊,以便我們可以將其用作圖像特徵提取器。我們將在該模型的末尾插入自己的全連接層,用於執行本文的分類任務。

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

現在我們將訓練該模型,在訓練過程中所用到的配置和回調函數與模型1中的類似,完整的代碼可以參考github鏈接。下圖展示了在訓練過程中,模型的準確度曲線和損失曲線。

醫生再添新助手!深度學習診斷傳染病 | 完整代碼+實操

Learning Curves for frozen pre-trained CNN

從上圖可以看出,該模型不像模型1中基本的 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

現在讓我們保存這個模型,很快我們將在測試集上用到它進行性能評估。

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'])


醫生再添新助手!深度學習診斷傳染病 | 完整代碼+實操

從圖中可以看到,第三個模型在測試集上的性能是最好的,其準確度和 f1-score 都達到了96%,這是一個非常好的結果,而且這個結果和論文中提到的更為複雜的模型所得到的結果具有相當的 可比性!

結論

本文研究了一個有趣的醫學影像案例——瘧疾檢測。瘧疾檢測是一個複雜的過程,而且能夠進行正確操作的醫療人員也很少,這是一個很嚴重的問題。本文利用 AI 技術構建了一個開源的項目,該項目在瘧疾檢測問題上具有最高的準確率,並使AI技術為社會帶來了效益。

相關鏈接:https://towardsdatascience.com/detecting-malaria-with-deep-learning-9e45c1e34b60


分享到:


相關文章: