使用Keras構建CNN網絡識別森林衛星圖

介紹

本文我們將使用tf.keras構建一個卷積神經網絡,用於識別森林衛星圖。tf.kerasTensorflow的高階API,具有模塊性,易擴展性,相比TensorflowLow-level API可以更快速的實現模型。Pytorch也是相當不錯的框架,感興趣的讀者可以查看官方文檔。伴隨TensorflowKeras的支持,目前Keras功能已十分強大,比如對TPU,多GPU的分佈式策略支持等。
Tensorflow編程環境

數據下載

下載鏈接(文件較大)

clipboard.png

圖片數據集包含4萬張照片,每張照片包含兩種標籤:

  • 天氣:晴天,陰天,霧霾等
  • 地面:農業區,居住區,道路等

下載鏈接

clipboard.png
CSV文件包含“圖片名字”,“天氣標籤”,“地面標籤”

我們希望訓練的模型可以準確的預測新衛星圖片的上述標籤。我們的模型將會有天氣,地面的兩個輸出,兩種不同的損失函數。訓練完成後,我們會導出模型使用Tensorflow Serving部署。

創建模型

import tensorflow as tf
IMG_SIZE=128

img_input=tf.keras.Input(shape=(IMG_SIZE,IMG_SIZE,3),name='input_layer')

conv_1_32=tf.keras.layers.Conv2D(
    filters=32,
    # 卷積核爲奇數:1,奇數具有中心點 2,圖像兩邊可以對稱padding
    kernel_size=3,
    padding='same',
    # 激活函數在輸入爲負值時激活值爲0,此時神經元無法學習,LeakyReLU在輸入爲負值時不爲0(但值很小)
    activation='relu'
)(img_input)
pool_1_2=tf.keras.layers.MaxPooling2D(
    # 默認過濾器大小和步長(2,2)
    padding='same'
)(conv_1_32)
conv_2_32=tf.keras.layers.Conv2D(
    filters=32,
    kernel_size=3,
    padding='same',
    activation='relu'
)(pool_1_2)
pool_2_2=tf.keras.layers.MaxPooling2D(
    padding='same'
)(conv_2_32)

# 將輸出展開
conv_flat=tf.keras.layers.Flatten()(pool_2_2)

fc_1_128=tf.keras.layers.Dense(
    units=128,
    activation='relu'
)(conv_flat)
# 僅訓練時設置
fc_1_drop=tf.keras.layers.Dropout(
    rate=0.2
)(fc_1_128)

fc_2_128=tf.keras.layers.Dense(
    units=128,
    activation='relu'
)(fc_1_drop)
fc_2_drop=tf.keras.layers.Dropout(
    rate=0.2
)(fc_2_128)

# 天氣標籤輸出
weather_output=tf.keras.layers.Dense(
    units=4,
    activation='softmax',
    name='weather'
)(fc_2_drop)
# 地面標籤輸出
ground_outpout=tf.keras.layers.Dense(
    units=13,
    # 對於類別大於2的分類問題,如果類別互斥使用softmax,反之使用sigmoid
    activation='sigmoid',
    name='ground'
)(fc_2_drop)

model=tf.keras.Model(
    inputs=img_input,
    outputs=[weather_output,ground_outpout]
)

各層參數數量

圖片描述

模型配置:

這裏有必要簡單介紹下Adam算法:

  • Adam 通過計算梯度的一階矩估計和二階矩估計而爲不同的參數設計獨立的自適應性學習率。
  • Adam 算法同時獲得了 AdaGrad 和 RMSProp 算法的優點。Adam 不僅如 RMSProp 算法那樣基於一階矩均值計算適應性參數學習率,它同時還充分利用了梯度的二階矩均值。
  • 梯度對角縮放的不變性
  • 適用於解決包含很高噪聲或稀疏梯度的問題
model.compile(
    # 也可嘗試下帶動量的SGD
    # Adam 默認參數值:lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=1e-08, decay=0.0.
    optimizer='adam',
    loss={
        # 注意這裏損失函數對應的激活函數
        "weather":'categorical_crossentropy',
        'ground':'binary_crossentropy'
    }

)

模型訓練

import ast
import numpy as np
import math
import os
import random
import pandas as pd
from tensorflow.keras.preprocessing.image import img_to_array
from tensorflow.keras.preprocessing.image import load_img

def load_image(img_path,img_size):
    # /255 將像素值由 0-255 轉爲 0-1 區間
    return img_to_array(load_img(img_path,target_size=(img_size,img_size)))/255.
class KagglePlanetSequence(tf.keras.utils.Sequence):
    def __init__(self,df_path,data_path,img_size,batch_size,mode='train'):
        self.df=pd.read_csv(df_path)
        self.img_size=img_size
        self.batch_size=batch_size
        self.mode=mode

        # ast.literal_eval(x) 功能同 eval,如:"[1,2,3]"轉爲[1,2,3],但增加了非法字符處理
        self.w_lables=self.df['weather_labels'].apply(lambda x:ast.literal_eval(x)).tolist()
        self.g_lables=self.df['ground_labels'].apply(lambda x:ast.literal_eval(x)).tolist()
        self.imges_list=self.df['image_name'].apply(lambda x:os.path.join(data_path,x+'.jpg')).tolist()

    def __len__(self):
        # math.ceil 向上取整,返回:大於或等於輸入數值
        # 計算每個epoch內訓練步數
        return int(math.ceil(len(self.df)/float(self.batch_size)))
    # 打亂數據
    def on_epoch_end(self):
        self.indexes=range(len(self.imges_list))
        if self.mode == 'train':
            self.indexes=random.sample(self.indexes,k=len(self.indexes))
    # 以下較簡單,別把區間算錯就好
    def get_batch_labels(self,idx):
        return [
            self.w_lables[idx*self.batch_size:(idx+1)*self.batch_size],
            self.g_lables[idx*self.batch_size:(idx+1)*self.batch_size]
        ]
    def get_batch_feature(self,idx):
        batch_images=self.imges_list[
            idx*self.batch_size:(idx+1)*self.batch_size
        ]
        return np.array([load_image(img,self.img_size) for img in batch_images])
    def __getitem__(self, idx):
        batch_x=self.get_batch_feature(idx)
        batch_y=self.get_batch_labels(idx)

        return batch_x,batch_y
        
 seq=KagglePlanetSequence('./KagglePlaneMCML.csv','./data/train/',
                         img_size=IMG_SIZE,batch_size=32)

在訓練期間通過添加callbacks,可以實現“保存模型”,‘提前停止訓練’等功能。本次,我們使用ModelCheckPoint在每迭代一次訓練集後保存模型。

callbacks=[
    # .h5 保存參數和圖
    # 1爲輸出進度條記錄,2爲每個epoch輸出一行記錄
    tf.keras.callbacks.ModelCheckpoint('./models.h5',verbose=1)
]

# fit_generator分批次產生數據,可以節約內存
model.fit_generator(
    generator=seq,
    verbose=1,
    epochs=1,
    # 使用基於進程的線程
    use_multiprocessing=True,
    # 進程數量
    workers=4,
    callbacks=callbacks
)

讀取已保存的模型,用於訓練

anther_model=tf.keras.models.load_model('./model.h5')
anther_model.fit_generator(generator=seq,verbose=1,epochs=1)

測試模型

test_sq=KagglePlanetSequence(
    './KagglePlaneMCML.csv',
    './data/train/',
    img_size=IMG_SIZE,
    batch_size=32,
    mode='test'
)
predictons=model.predict_generator(
    generator=test_sq,verbose=1
)

使用DataSet作爲輸入

TFRecord文件中的數據是通過tf.train.Example Protocol Buffer格式存儲,其中包含一個從屬性名稱到取值的字典,屬性的取值可以爲”BytesList“,”FloatList“或者”Int64List“。此外,TFRecord的值可以作爲Cloud MLEngine的輸入。

我們首先將圖片和標籤保存爲TFRecord文件

def _bytes_feature(value):
    return tf.train.Feature(bytes_list=tf.train.BytesList(value=[value]))
tf_records_filename='./data/KajgglePlaneTFRecord_{}'.format(IMG_SIZE)
writer=tf.python_io.TFRecordWriter(tf_records_filename)

# 獲取對應數據
df_train={}
img_list=[os.path.join('./data/train',v+'.jpg') for v in df_train['image_name'].tolist()]
w_lables_arr=np.array([ast.literal_eval(l) for l in df_train['weather_labels']])
g_lables_arr=np.array([ast.literal_eval(l) for l in df_train['ground_labels']])

# 文件寫入
for i in range(len(df_train)):
    w_labels=w_lables_arr[i]
    g_lables=g_lables_arr[i]
    img=np.array([load_image(img_list[i],IMG_SIZE)])

    example=tf.train.Example(
        features=tf.train.Feature(
        # 讀取的時候使用該key
            feattures={
                'image':_bytes_feature(img.tostring()),
                'weather_labels':_bytes_feature(w_lables_arr.tostring()),
                'ground_lables':_bytes_feature(g_lables_arr.tostring())
            }
        )
    )
    writer.write(example.SerizlizeToString())
writer.close()

DataSet讀取TFRecord文件

”“”
提供兩種解析方法,一種是tf.FixedLenFeature,解析結果爲tensor,另一種是tf.VarLenFeature得到的結果是SparseTensor,用於處理稀疏數據。當然,讀取數據和寫入數據的格式要一致。
“”“
featdef={
    'image':tf.FixedLenFeature(shape=[],dtype=tf.string),
    'weather_lables':tf.FixedLenFeature(shape=[],dtype=tf.string),
    'ground_labels':tf.FixedLenFeature(shape=[],dtype=tf.string)
}
def _parse_record(tfre_file,clip=False):
    file=tf.parse_single_example(tfre_file,features=featdef)
    # tf.decode_raw 將字符串解析成圖像對應的像素數組
    img=tf.reshape(tf.decode_raw(file['image'],tf.float32),shape=(IMG_SIZE,IMG_SIZE,3))
    
    weather=tf.decode_raw(file['weather_lables'],tf.float32)
    ground=tf.decode_raw(file['ground_lables'],tf.float32)

    return img,weather,ground
ds_train=tf.data.TFRecordDataset(filenames='./data/KanglePlaneTFRecord_{}'.format(IMG_SIZE),map=_parse_record)
ds_train=ds_train.shuffle(buffer_size=1000).batch(32)

模型訓練

model=tf.keras.Model(inputs=img_input,outputs=[weather_output,ground_outpout])
model.compile(
    optimizer='adam',
    loss={
        'weather':'categorical_crossentropy',
        'ground':'binary_crossentropy'
    }
)
# history.history:loss values and metrics values
# 通過history_rest.history可以獲取loss value metrics value等信息
history_rest=model.fit(ds_train,steps_per_epoch=100,epochs=1)

模型導出

Tensorflow Serving框架
圖片描述
如需瞭解更多有關內容,請查看官網介紹。
使用TensorFlow Serving 部署我們只能使用SaveModel方法。

# 模型導出,官方推薦使用save_model
# 如果需要更多自定義功能請使用SavedModelBuilder
# 返回訓練模式/測試模式的flag
# learning_phase: 0 train model 1 test model
tf.keras.backend.set_learning_phase(1)

model=tf.keras.models.load_model('./model.h5')
export_path='./PlaneModel/1'

with tf.keras.backend.get_session() as sess:
    tf.saved_model.simple_save(
        session=sess,
        export_dir=export_path,
        inputs={
            'input_image':model.input
        },
        outputs={
            t.name:t for t in model.outputs
        }
    )

模型保存後的文件結構:

$ tree
.
└── 1
    ├── saved_model.pb
    └── variables
        ├── variables.data-00000-of-00001
        └── variables.index

重新訓練的模型可以放到”PlanetModel/2“文件夾內,此時TensorFlow Serving會自動更新。

TensorFlow Serving安裝

1.安裝Docker

2.安裝Serving image

docker pull tensorflow/serving

3,運行Tensorflow Serving

  • gRPC默認端口:8500
  • REST API默認端口:8501
  • 默認環境變量MODEL_NAME:"model" MODEL_BASE_PATH:"/models"
tensorflow_model_server --model_base_path=$(pwd) --rest_api_port=9000 --model_name=PlanetModel

4,模型預測

  • 請求格式要求
{
  # 如多人開發,最好自定義
  "signature_name": <string>,

  # 只可包含以下任意一項
  "instances": <value>|<(nested)list>|<list-of-objects>
  "inputs": <value>|<(nested)list>|<object>
}
import requests
import json

# 不要忘了歸一化處理
image = img_to_array(load_img('./data/train/train_10001.jpg', target_size=(128,128))) / 255.
payload={
    'instances':[{'input_images':image.tolist()}]
}
result=requests.post('http://localhost:9000/v1/models/PlanetModel:predict',json=payload)
json.load(result.content)

5,預測結果

  • 返回格式
{
  "predictions": <value>|<(nested)list>|<list-of-objects>
}
{u'predictions': [
     {u'ground_2/Sigmoid:0': [
        0.153237,
        0.000527727,
        0.00555856,
        0.00542973,
        0.00105254,
        0.000256282,
        0.103614,
        0.0325185,
        0.998204,
        0.072204,
        0.00745501,
        0.00326175,
        0.0942268],
   u'weather_2/Softmax:0': [
        0.963947,
        0.000207846,
        0.00113924,
        0.0347063]
     }]}

總結

本次項目只是簡單的案例,主要是爲了熟悉相關模塊的使用,項目本身還有很多需要優化的地方。keras清晰,友好可以快速實現你的想法,還可以結和“Eager Execution” “Estimator”使用。雖然keras優點很多,但由於它高度封裝,所以想進一步瞭解Tensorflow的朋友,掌握Low-level API還是很有必要的。

本文實現參考Stijn Decubber的文章,歡迎關注他的博客。

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