通過遷移學習實現OCT圖像識別

遷移學習

圖片描述
遷移學習就是用別人已經訓練好的模型,如:Inception Model,Resnet Model等,把它當做Pre-trained Model,幫助我們提取特徵。常用方法是去除Pre-trained Model的最後一層,按照自己的需求重新更改,然後用訓練集訓練。
因爲Pre-trained Model可能已經使用過大量數據集,經過了長時間的訓練,所以我們通過遷移學習可以使用較少的數據集訓練就可以獲得相對不錯的結果。

由於項目中使用到Estimator,所以我們再簡單介紹下Estimator。

TF Estimator

這裏引用下官網 Estimator的介紹。

  • 您可以在本地主機上或分佈式多服務器環境中運行基於 Estimator 的模型,而無需更改模型。此外,您可以在 CPU、GPUTPU 上運行基於 Estimator 的模型,而無需重新編碼模型。
  • Estimator 簡化了在模型開發者之間共享實現的過程。
  • 您可以使用高級直觀代碼開發先進的模型。簡言之,採用 Estimator 創建模型通常比採用低階 TensorFlow API 更簡單。
  • Estimator 本身在 tf.layers 之上構建而成,可以簡化自定義過程。
  • Estimator 會爲您構建圖。
  • Estimator 提供安全的分佈式訓練循環,可以控制如何以及何時:構建圖,初始化變量,開始排隊,處理異常,創建檢查點並從故障中恢復,保存TensorBoard的摘要。
  • 使用 Estimator 編寫應用時,您必須將數據輸入管道從模型中分離出來。這種分離簡化了不同數據集的實驗流程。

案例

我們可以使用“tf.keras.estimator.model_to_estimator”keras轉換Estimator。這裏使用的數據集是Fashion-MNIST。

Fashion-MNIST數據標籤:

圖片描述

數據導入:

import  os
import time
import tensorflow as tf
import numpy as np
import tensorflow.contrib as tcon

(train_image,train_lables),(test_image,test_labels)=tf.keras.datasets.fashion_mnist.load_data()
TRAINING_SIZE=len(train_image)
TEST_SIZE=len(test_image)

# 將像素值由0-255 轉爲0-1 之間
train_image=np.asarray(train_image,dtype=np.float32)/255
# 4維張量[batch_size,height,width,channels]
train_image=train_image.reshape(shape=(TRAINING_SIZE,28,28,1))
test_image=np.asarray(test_image,dtype=np.float32)/255
test_image=test_image.reshape(shape=(TEST_SIZE,28,28,1))

使用tf.keras.utils.to_categorical將標籤轉爲獨熱編碼表示:

# lables 轉爲 one_hot表示
# 類別數量
LABEL_DIMENSIONS=10
train_lables_onehot=tf.keras.utils.to_categorical(
    y=train_lables,num_classes=LABEL_DIMENSIONS
)
test_labels_onehot=tf.keras.utils.to_categorical(
    y=test_labels,num_classes=LABEL_DIMENSIONS
)
train_lables_onehot=train_lables_onehot.astype(np.float32)
test_labels_onehot=test_labels_onehot.astype(np.float32)

創建Keras模型:

“”“
3層卷積層,2層池化層,最後展平添加全連接層使用softmax分類
”“”
inputs=tf.keras.Input(shape=(28,28,1))
conv_1=tf.keras.layers.Conv2D(
    filters=32,
    kernel_size=3,
    # relu激活函數在輸入值爲負值時,激活值爲0,此時可以使用LeakyReLU
    activation=tf.nn.relu
)(inputs)
pool_1=tf.keras.layers.MaxPooling2D(
    pool_size=2,
    strides=2
)(conv_1)
conv_2=tf.keras.layers.Conv2D(
    filters=64,
    kernel_size=3,
    activation=tf.nn.relu
)(pool_1)
pool_2=tf.keras.layers.MaxPooling2D(
    pool_size=2,
    strides=2
)(conv_2)
conv_3=tf.keras.layers.Conv2D(
    filters=64,
    kernel_size=3,
    activation=tf.nn.relu
)(pool_2)

conv_flat=tf.keras.layers.Flatten()(conv_3)
dense_64=tf.keras.layers.Dense(
    units=64,
    activation=tf.nn.relu
)(conv_flat)

predictions=tf.keras.layers.Dense(
    units=LABEL_DIMENSIONS,
    activation=tf.nn.softmax
)(dense_64)

模型配置:

model=tf.keras.Model(
    inputs=inputs,
    outputs=predictions
)
model.compile(
    loss='categorical_crossentropy',
    optimizer=tf.train.AdamOptimizer(learning_rate=0.001),
    metrics=['accuracy']
)

創建Estimator

指定GPU數量,然後將keras轉爲Estimator,代碼如下:

NUM_GPUS=2
strategy=tcon.distribute.MirroredStrategy(num_gpus=NUM_GPUS)
config=tf.estimator.RunConfig(train_distribute=strategy)

estimator=tf.keras.estimator.model_to_estimator(
    keras_model=model,config=config
)

前面說到過使用 Estimator 編寫應用時,您必須將數據輸入管道從模型中分離出來,所以,我們先創建input function。使用prefetchdata預置緩衝區可以加快數據讀取。因爲下面的遷移訓練使用的數據集較大,所以在這裏有必要介紹下優化數據輸入管道的相關內容。

優化數據輸入管道

TensorFlow數據輸入管道是以下三個過程:

  • Extract:數據讀取,如本地,服務端
  • Transform:使用CPU處理數據,如圖片翻轉,裁剪,數據shuffle等
  • Load:將數據轉給GPU進行計算

數據讀取:

通常,當CPU爲計算準備數據時,GPU/TPU處於閒置狀態;當GPU/TPU運行時,CPU處於閒置,顯然設備沒有被合理利用。
圖片描述
tf.data.Dataset.prefetch可以將上述行爲並行實現,當GPU/TPU執行第N次訓練,此時讓CPU準備N+1次訓練使兩個操作重疊,從而利用設備空閒時間。
圖片描述
通過使用tf.contrib.data.parallel_interleave可以並行從多個文件讀取數據,並行文件數有cycle_length指定。
數據轉換:

使用tf.data.Dataset.map對數據集中的數據進行處理,由於數據獨立,所以可以並行處理。此函數讀取的文件是含有確定性順序,如果順序對訓練沒有影響,也可以取消確定性順序加快訓練。
圖片描述

def input_fn(images,labels,epochs,batch_size):
    ds=tf.data.Dataset.from_tensor_slices((images,labels))
    # repeat值爲None或者-1時將無限制迭代
    ds=ds.shuffle(500).repeat(epochs).batch(batch_size).prefetch(batch_size)

    return ds

模型訓練

# 用於計算迭代時間
class TimeHistory(tf.train.SessionRunHook):
    def begin(self):
        self.times = []
    def before_run(self, run_context):
        self.iter_time_start = time.time()
    def after_run(self, run_context, run_values):
        self.times.append(time.time() - self.iter_time_start)

time_hist = TimeHistory()
BATCH_SIZE = 512
EPOCHS = 5
# lambda爲了填寫參數
estimator.train(lambda:input_fn(train_images,
                                train_labels,
                                epochs=EPOCHS,
                                batch_size=BATCH_SIZE),
                hooks=[time_hist])

# 訓練時間
total_time = sum(time_hist.times)
print(f"total time with {NUM_GPUS} GPU(s): {total_time} seconds")

# 訓練數據量
avg_time_per_batch = np.mean(time_hist.times)
print(f"{BATCH_SIZE*NUM_GPUS/avg_time_per_batch} images/second with
        {NUM_GPUS} GPU(s)")

結果如圖:

圖片描述

得益於Estimator數據輸入和模型的分離,評估方法很簡單。

estimator.evaluate(lambda:input_fn(test_images, 
                                   test_labels,
                                   epochs=1,
                                   batch_size=BATCH_SIZE))

遷移學習訓練新模型

我們使用Retinal OCT images數據集進行遷移訓練,數據標籤爲:NORMAL, CNV, DME DRUSEN,包含分辨率爲512*296,84495張照片。
圖片描述

數據讀取,設置input_fn:

labels = ['CNV', 'DME', 'DRUSEN', 'NORMAL']
train_folder = os.path.join('OCT2017', 'train', '**', '*.jpeg')
test_folder = os.path.join('OCT2017', 'test', '**', '*.jpeg')
def input_fn(file_pattern, labels,
             image_size=(224,224),
             shuffle=False,
             batch_size=64, 
             num_epochs=None, 
             buffer_size=4096,
             prefetch_buffer_size=None):
    # 創建查找表,將string 轉爲 int 64ID
    table = tcon.lookup.index_table_from_tensor(mapping=tf.constant(labels))
    num_classes = len(labels)

    def _map_func(filename):
        # sep = '/'
        label = tf.string_split([filename], delimiter=os.sep).values[-2]
        image = tf.image.decode_jpeg(tf.read_file(filename), channels=3)
        image = tf.image.convert_image_dtype(image, dtype=tf.float32)
        image = tf.image.resize_images(image, size=image_size)
        # tf.one_hot:根據輸入的depth返回one_hot張量
        # indices = [0, 1, 2]
        # depth = 3
        # tf.one_hot(indices, depth) return:
        # [[1., 0., 0.],
        #  [0., 1., 0.],
        #  [0., 0., 1.]]
        return (image, tf.one_hot(table.lookup(label), num_classes))
    
    dataset = tf.data.Dataset.list_files(file_pattern, shuffle=shuffle)

    if num_epochs is not None and shuffle:
        dataset = dataset.apply(
            tcon.data.shuffle_and_repeat(buffer_size, num_epochs))
    elif shuffle:
        dataset = dataset.shuffle(buffer_size)
    elif num_epochs is not None:
        dataset = dataset.repeat(num_epochs)
   
    dataset = dataset.apply(
        tcon.data.map_and_batch(map_func=_map_func,
                                      batch_size=batch_size,
                                      num_parallel_calls=os.cpu_count()))
    dataset = dataset.prefetch(buffer_size=prefetch_buffer_size)
    
    return dataset

使用VGG16網絡

通過keras使用預訓練的VGG16網絡,我們重訓練最後5層:

# include_top:不包含最後3個全連接層
keras_vgg16 = tf.keras.applications.VGG16(input_shape=(224,224,3),
                                          include_top=False)
output = keras_vgg16.output
output = tf.keras.layers.Flatten()(output)
prediction = tf.keras.layers.Dense(len(labels),
                                   activation=tf.nn.softmax)(output)
model = tf.keras.Model(inputs=keras_vgg16.input,
                       outputs=prediction)
# 後5層不訓練
for layer in keras_vgg16.layers[:-4]:
    layer.trainable = False

重新訓練模型:

# 通過遷移學習得到模型
model.compile(loss='categorical_crossentropy', 
              # 使用默認學習率
              optimizer=tf.train.AdamOptimizer(),
              metrics=['accuracy'])
NUM_GPUS = 2
strategy = tf.contrib.distribute.MirroredStrategy(num_gpus=NUM_GPUS)
config = tf.estimator.RunConfig(train_distribute=strategy)

# 轉至estimator
estimator = tf.keras.estimator.model_to_estimator(model,
                                                  config=config)
BATCH_SIZE = 64
EPOCHS = 1
estimator.train(input_fn=lambda:input_fn(train_folder,
                                         labels,
                                         shuffle=True,
                                         batch_size=BATCH_SIZE,
                                         buffer_size=2048,
                                         num_epochs=EPOCHS,
                                         prefetch_buffer_size=4),
                hooks=[time_hist])
# 模型評估:
estimator.evaluate(input_fn=lambda:input_fn(test_folder,
                                            labels, 
                                            shuffle=False,
                                            batch_size=BATCH_SIZE,
                                            buffer_size=1024,
                                            num_epochs=1))

VGG16網絡

v2-266bf05d27220b875849507a98703791_b.jpg

如圖所示,VGG16有13個卷積層和3個全連接層。VGG16輸入爲[224,224,3],卷積核大小爲(3,3),池化大小爲(2,2)步長爲2。各層的詳細參數可以查看VGG ILSVRC 16 layers因爲圖片較大,這裏只給出部分截圖,詳情請點擊鏈接查看。
圖片描述
圖片描述

VGG16模型結構規整,簡單,通過幾個小卷積核(3,3)卷積層組合比大卷積核如(7,7)更好,因爲多個(3,3)卷積比一個大的卷積擁有更多的非線性,更少的參數。此外,驗證了不斷加深的網絡結構可以提升性能(卷積+卷積+卷積+池化,代替卷積+池化,這樣減少W的同時有可以擬合更復雜的數據),不過VGG16參數量很多,佔用內存較大。

總結

通過遷移學習我們可以使用較少的數據訓練出來一個相對不錯的模型,Estimator簡化了機器學習編程特別是在分佈式環境下。對於輸入數據較多的情況我們要從Extract,Transform,Load三方面考慮進行優化處理。當然,除了VGG16我們還有很多選擇,如:Inception Model,Resnet Model。

代碼實現部分參考Kashif Rasul,在此表示感謝。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章