使用Keras构建CNN网络识别森林卫星图

介绍

本文我们将使用tf.keras构建一个卷积神经网络,用于识别森林卫星图。tf.kerasTensorflow的高阶API,具有模块性,易扩展性,相比TensorflowLow-level API可以更快速的实现模型。Pytorch也是相当不错的框架,感兴趣的读者可以查看官方文档。伴随TensorflowKeras的支持,目前Keras功能已十分强大,比如对TPU,多GPU的分布式策略支持等。
使用Keras构建CNN网络识别森林卫星图

数据下载

下载链接(文件较大)

使用Keras构建CNN网络识别森林卫星图

图片数据集包含4万张照片,每张照片包含两种标签:

  • 天气:晴天,阴天,雾霾等
  • 地面:农业区,居住区,道路等

下载链接

使用Keras构建CNN网络识别森林卫星图
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]
)

各层参数数量

使用Keras构建CNN网络识别森林卫星图

模型配置:

这里有必要简单介绍下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框架
使用Keras构建CNN网络识别森林卫星图
如需了解更多有关内容,请查看官网介绍。
使用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的文章,欢迎关注他的博客。

相关推荐