乐趣区

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

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

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

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

下载链接
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 的文章,欢迎关注他的博客。

退出移动版