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

遷移學習

圖片描述
遷移學習就是用別人已經訓練好的模型,如:Inception Model,Resnet Model等,把它當作Pre-trained Model,幫助咱們提取特徵。經常使用方法是去除Pre-trained Model的最後一層,按照本身的需求從新更改,而後用訓練集訓練。
由於Pre-trained Model可能已經使用過大量數據集,通過了長時間的訓練,因此咱們經過遷移學習可使用較少的數據集訓練就能夠得到相對不錯的結果。git

因爲項目中使用到Estimator,因此咱們再簡單介紹下Estimator。github

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將標籤轉爲獨熱編碼表示:app

# 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,代碼以下:ide

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,在此表示感謝。

相關文章
相關標籤/搜索