【Keras】一文弄懂如何使用Keras打造一个实时可用交通标志
深度学习这几年是一个很火的技术,也有很多涌入这个领域,对于新手来说,入门很容易,训练一个简单的模型,调下参数谁都会。对于任何一个公司主要有高质量的数据,随便一个研究生,本科生都能复现大神的模型直接部署到应用上。困难的是如何找到应用的方向,未来深度学习的趋势也肯定是比拼如何快速优质应用这些技术为产品服务。我在这个方向也接近有一年经验了。最开始做的是缺陷检测,接触最多的是分类模型,后来也逐渐接触分割,检测等等,在这个过程中自己也踩了不少坑,也学了不少东西,从最开始的在服务器上追求精度,到现在直接在移动端部署模型,效率精度兼备。这篇文章主要是以交通标志分类为例给新人提供一个深度学习从数据查找,模型训练,以及应用到生产环境整个流程的思路。
大纲
数据来源
模型训练
模型转换
模型部署
数据来源
对于我们做应用的人而言,最重要的应该就是数据。数据往往是一个算法公司的主要财产之一。那么如何为自己的问题获取对应的数据呢?先说结论:大型公开数据集 > 迁移学习 > 自己标注。
如果自己做的问题有大型公开数据集最好,那么直接用大型数据集就行,免去自己查找数据的麻烦,只需要专注于选模型,调参数等。这里给几个个CV方向的数据查找网址:
- Google最近推出的数据集搜索引擎
https://toolbox.google.com/datasetsearch - Kaggle 上面的一些比赛也会有一些公开数据
https://www.kaggle.com - Google检索
这就主要考验一个人的检索能力了,翻墙是必然的。
当然使用公开数据集的时候也要遵守相应的规范,看是不是可以直接拿来商用。这方面其实中国公司都不怎么注意。
如果没有大型公开数据集就要看有没有小一点的数据集,然后在这个基础上使用在大型数据集上训练好的模型权重进行迁移学习。
最开始来现在这个公司的时候,给公司做一个人像分割模型。和最新版微信的制作自己的表情背后的技术实现是一致的。但是我们有的数据只有2000张左右,先使用Pascal数据训练一个物体分割数据模型,然后在这个基础上使用我们自己的人像数据迁移学习一个人像模型,最后取得了比较好的效果。可以参考我另一篇文章:
Tensorflow移动端模型转换
实在没有办法可以自己标注数据,但是这个是成本很大的问题,还有准确度的问题。当然这只是针对小公司而言,对于大公司数据也是壁垒之一。
这篇文章我们使用一个交通标志数据集。
下载训练和测试数据:
BelgiumTSC_Training (171.3MBytes)
BelgiumTSC_Testing (76.5MBytes)
下载后分别命名为train/val文件夹放在traffic_sign目录下:
.
├── data
│ └── traffic_sign
│ ├── train
│ └── val
注意这里我们只将这个数据用来学习使用。
获得数据之后,最好大致检查一遍所有的数据,观察下数据质量,统计下每个类的数目,这样数据有个大致的了解。
模型训练
这里我们使用Keras框架,Tensorflow作为后端来进行训练,其实对于一般做移动端应用的公司我觉得使用Keras,然后转换到移动端推理框架挺方便的。
这里就不介绍Keras使用了,有需要的童鞋可以参考我的其他文章,介绍了很多Keras使用。这里提供一个训练网络和读取数据的易用接口,省去很多重复性工作:
train.py
"""
Easy to use train script for different kinds of networkds and dataset...
@author: Vincent
"""
import os
import glob
from collections import Counter
import numpy as np
import keras
from keras.optimizers import SGD
import keras.backend as K
from keras.models import load_model
from keras.preprocessing.image import ImageDataGenerator
from keras import callbacks
import argparse
from simplenet import SimpleNet
from learning_rate import create_lr_schedule
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument(
'--dataset',
type=str,
default='traffic_sign',
help='directory name of dataset, which should have structure ./train ./val and according classes to suit flow from directory'
)
ap.add_argument(
'--batch_size',
type=int,
default=16,
help='training batch size'
)
ap.add_argument(
'--input_shape',
type=list,
default=(112,112,3),
help='input image shape',
)
ap.add_argument(
'--epochs',
type=int,
default=100,
help='training epochs'
)
ap.add_argument(
'--class_weight_balance_mode',
type=bool,
default=True,
help='whether to enable class weights mode to deal with classs unbalance'
)
ap.add_argument(
'--model',
type=str,
default="SimpleNet",
help="which model to use to train"
)
args = vars(ap.parse_args())
num_classes = len([f for f in os.listdir(os.path.join('/Users/yuhua.cheng/Opt/temp/traffic_sign/data/{0}'.format(args['dataset']),'train'))
if os.path.isdir(os.path.join('/Users/yuhua.cheng/Opt/temp/traffic_sign/data/{0}/train/'.format(args['dataset']),f))])
print("num_classes:", num_classes)
num_train_samples = len(glob.glob('/Users/yuhua.cheng/Opt/temp/traffic_sign/data/{0}/train/*/*.ppm'.format(args['dataset'])))
num_val_samples = len(glob.glob('/Users/yuhua.cheng/Opt/temp/traffic_sign/data/{0}/val/*/*.ppm'.format(args['dataset'])))
if args['class_weight_balance_mode']:
trained_model_path = './models/{0}_with_class_weights.h5'.format(args['dataset'])
else:
trained_model_path = './models/{0}_without_class_weights.h5'.format(args['dataset'])
train_gen = ImageDataGenerator(
rescale = 1/255.,
samplewise_center=True,
samplewise_std_normalization=True,
rotation_range=15,
zoom_range=0.15,
width_shift_range=0.1,
height_shift_range=0.1,
horizontal_flip=True,
)
val_gen = ImageDataGenerator(
rescale = 1/255.,
samplewise_center=True,
samplewise_std_normalization=True
)
train_iter = train_gen.flow_from_directory('/Users/yuhua.cheng/Opt/temp/traffic_sign/data/{0}/train'.format(args['dataset']),
target_size=args['input_shape'][0:2],
batch_size=args['batch_size'],
# color_mode='grayscale',
# save_to_dir='./aug_train',
class_mode='categorical',
interpolation='bicubic')
val_iter = train_gen.flow_from_directory('/Users/yuhua.cheng/Opt/temp/traffic_sign/data/{0}/val'.format(args['dataset']),
target_size=args['input_shape'][0:2],
batch_size=args['batch_size'],
# color_mode='grayscale',
# save_to_dir='./aug_val',
class_mode='categorical',
interpolation='bicubic')
# 针对样本不均衡问题进行weight balance
class_weight = {}
counter = Counter(train_iter.classes)
max_val = float(max(counter.values()))
class_weights = {class_id:max_val/num_images for class_id, num_images in counter.items()}
print("class_weights for samples:", class_weights)
#
model = locals()[args['model']](input_shape=args['input_shape'], num_classes=num_classes)
# sgd = SGD(lr=1e-1, decay=1e-6, momentum=0.9, nesterov=True)
sgd = keras.optimizers.Adadelta()
# create callbacks
tensorboard = callbacks.TensorBoard(log_dir='./logs', write_graph=False)
learning_rate = callbacks.LearningRateScheduler(create_lr_schedule(epochs=args['epochs'], lr_base=0.01, mode='progressive_drops'))
callbacks = [tensorboard, learning_rate]
# compile the model
model.compile(optimizer=sgd, loss='categorical_crossentropy', metrics=['accuracy'])
# train the model
if args['class_weight_balance_mode']:
history = model.fit_generator(
generator = train_iter,
steps_per_epoch = num_train_samples // args['batch_size'],
epochs=args['epochs'],
validation_data = val_iter,
validation_steps = num_val_samples // args['batch_size'],
class_weight = class_weights,
verbose = 1,
callbacks = callbacks)
else:
history = model.fit_generator(
generator = train_iter,
steps_per_epoch = num_train_samples // args['batch_size'],
epochs = args['epochs'],
validation_data = val_iter,
validation_steps = num_val_samples // args['batch_size'],
verbose = 1,
callbacks = callbacks)
model.save(trained_model_path)
我的网络结构:
simplenet.py
"""
my simplenet for experiments
"""
import keras
from keras.models import load_model, Model
from keras import regularizers, optimizers
from keras.layers import Input, Conv2D, Activation, Dense, Flatten
from keras.layers import BatchNormalization, Dropout
from keras.layers import MaxPooling2D, GlobalMaxPooling2D, GlobalAveragePooling2D
from keras.datasets import cifar10
def conv2d_bn_drop(x, filters, kernel_size=3, strides=1, padding='same', activation='relu', use_bias=False, dropout_rate=0, name=None):
"""Utility fucntion to apply conv + BN + dropout
# Arguments:
# Returns:
Output tensor after applying 'Conv2D' and 'BatchNormalization' and "DropOut'
"""
if name is not None:
conv_name = name + '_conv'
bn_name = name + '_bn'
drop_name = name + '_dropout'
ac_name = name + '_' + activation
else:
conv_name = None
bn_name = None
drop_name = name + '_dropout'
x = Conv2D(filters, kernel_size, strides=strides, padding=padding, use_bias=use_bias, name=conv_name)(x)
x = BatchNormalization(axis=-1, scale=False, name=bn_name)(x)
x = Activation(activation, name=ac_name)(x)
x = Dropout(rate=dropout_rate, name=drop_name)(x)
return x
def conv2d_bn_pooling_drop(x, filters, kernel_size=3, strides=1, padding='same', activation='relu', use_bias=False, pooling="max", dropout_rate=0, name=None):
"""Utility fucntion to apply conv + BN + dropout
# Arguments:
# Returns:
Output tensor after applying 'Conv2D' and 'BatchNormalization' and "DropOut'
"""
if name is not None:
conv_name = name + '_conv'
bn_name = name + '_bn'
drop_name = name + '_dropout'
ac_name = name + '_' + activation
else:
conv_name = None
bn_name = None
drop_name = name + '_dropout'
x = Conv2D(filters, kernel_size, padding=padding, use_bias=use_bias, name=conv_name)(x)
x = BatchNormalization(axis=-1, scale=False, name=bn_name)(x)
if pooling == 'max':
x = MaxPooling2D(pool_size=(2,2), strides=2, padding='valid')(x)
else:
x = AveragePooling2D(pool_size=(2,2), strides=2, padding='valid')(x)
x = Activation(activation, name=ac_name)(x)
x = Dropout(rate=dropout_rate, name=drop_name)(x)
return x
def conv2d_pooling_bn_drop(x, filters, kernel_size=3, strides=1, padding='same', activation='relu', use_bias=False, pooling="max", dropout_rate=0, name=None):
"""Utility fucntion to apply conv + BN + dropout
# Arguments:
# Returns:
Output tensor after applying 'Conv2D' and 'BatchNormalization' and "DropOut'
"""
if name is not None:
conv_name = name + '_conv'
bn_name = name + '_bn'
drop_name = name + '_dropout'
ac_name = name + '_' + activation
else:
conv_name = None
bn_name = None
drop_name = name + '_dropout'
x = Conv2D(filters, kernel_size, padding=padding, use_bias=use_bias, name=conv_name)(x)
if pooling == 'max':
x = MaxPooling2D(pool_size=(2,2), strides=2, padding='valid')(x)
else:
x = AveragePooling2D(pool_size=(2,2), strides=2, padding='valid')(x)
x = BatchNormalization(axis=-1, scale=False, name=bn_name)(x)
x = Activation(activation, name=ac_name)(x)
x = Dropout(rate=dropout_rate, name=drop_name)(x)
return x
def SimpleNet(input_tensor=None, stride=2, weight_decay=1e-2, pooling="Max", act='relu',
input_shape=(227,227,3), num_classes=10):
s = stride
act = 'relu'
if input_tensor is None:
input_tensor = Input(shape=input_shape)
x = conv2d_bn_drop(input_tensor, 64, (7,7), strides=2, padding='same', activation='relu', name="block1_0")
x = conv2d_bn_drop(x, 64, (3,3), padding='same', activation='relu', name="block1_1")
x = conv2d_bn_drop(x, 96, (3,3), padding='same', activation='relu', name="block2_0")
x = conv2d_bn_pooling_drop(x, 96, (3,3), padding='same', activation='relu', name="block2_1")
x = conv2d_bn_drop(x, 96, (3,3), padding='same', activation='relu', name="block2_2")
x = conv2d_bn_drop(x, 128, (3,3), padding='same', activation='relu', name="block3_0")
x = conv2d_pooling_bn_drop(x, 128, (3,3), padding='same', activation='relu', name="block4_0")
x = conv2d_bn_drop(x, 160, (3,3), padding='same', activation='relu', name="block4_1")
x = conv2d_bn_pooling_drop(x, 160, (3,3), padding='same', activation='relu', dropout_rate=0.3, name="block4_2")
x = Conv2D(filters=256, kernel_size=(3,3), strides=1, padding="same", activation='relu', name='block5_0_conv')(x)
x = Conv2D(filters=512, kernel_size=(3,3), strides=1, padding="same", activation='relu', name='cccp5')(x)
x = MaxPooling2D(pool_size=(2,2), strides=2, padding='valid', name='poolcp5')(x)
x = Conv2D(filters=512, kernel_size=(3,3), strides=2, padding="same", activation='relu', name='cccp6')(x)
x = GlobalAveragePooling2D()(x)
x = Dense(num_classes)(x)
x = Activation('softmax')(x)
model = Model(inputs=input_tensor, outputs=x)
model.summary()
return model
if __name__ == '__main__':
input_tensor = Input(shape=(227, 227,3))
model = SimpleNet(input_tensor)
准备好数据和网络配置文件之后在tran.py训练脚本中传入相应的参数,直接训练便可。
训练100 epochs之后就有0.945-0.95的准确度了,说明我们的模型效果还可以。
image.png
训练好模型之后一般需要在真实环境测试一下:
测试脚本:
import cv2
import os
import glob
import numpy as np
from matplotlib import pyplot as plt
from keras.models import load_model
from imageio import imread
image_files = [f for f in os.listdir('./data/traffic_sign/test') if not f.startswith('.')]
classes = sorted(os.listdir('./data/traffic_sign/val'))
model = load_model('./models/traffic_sign_with_class_weights.h5')
model.summary()
for image_file in image_files:
img = imread(os.path.join('./data/traffic_sign/test', image_file))
plt.subplot(1,2,1)
plt.imshow(img)
plt.title("img")
img = cv2.resize(img, (112,112))
img = img.astype("float32")
img = (img - np.mean(img)) / np.std(img)
img = np.expand_dims(img, 0)
label = np.argmax(model.predict(img))
label_image = imread(glob.glob('./data/traffic_sign/train/{0}/*.ppm'.format(classes[label]))[0])
plt.subplot(1,2,2)
plt.imshow(label_image)
plt.title("predicted img")
plt.show()
Screen Shot 2019-01-05 at 12.00.15 PM.png
Screen Shot 2019-01-05 at 12.02.18 PM.png
看起来还可以哈
一般很多人的文章调完参数,达到一定的准确度,观察一些测试数据,就不介绍了。然而你有这个模型,如何将它应用到生产环境中还有一段路要走。接下来的部分就介绍如何将训练好的模型移植到移动端,打造一个真正实时可用的App。
模型转换
这一小结介绍如何将模型转换到移动端可用框架。
现有的移动端推理框架有很多,如CoreML, tensorflow lite,Caffe2等。需要了解的话,可以参考下我这篇文章: Tensorflow移动端模型转换。国内ncnn的口碑和速度算是比较好的了,用的人也比较多。这里我们选用苹果自带的CoreML,CoreML入门比较简单,不需要太多配置,将模型格式转化正确便可,笔者不是做IOS开发的,也是在前人的基础上进行一些修改。
我们遇到的第一个问题是需要将Keras模型转换到CoreML可用的格式, 这里提供一个转换脚本(版本不同会有接口的变换, 这里是python2, Keras 2.1.6, tensorflow 1.12.0):
import coremltools
import keras
from keras.models import load_model
from keras.utils.generic_utils import CustomObjectScope
class_labels = []
for i in range(62):
class_labels.append(str(i))
with CustomObjectScope({'relu6': keras.applications.mobilenet.relu6}):
keras_model = load_model('traffic_sign_with_class_weights.h5')
coreml_model = coremltools.converters.keras.convert(keras_model,
input_names=['input_1'],
image_input_names='input_1',
output_names='activation_1',
image_scale=2/255.0,
red_bias=-1,
green_bias=-1,
blue_bias=-1,
class_labels=class_labels)
coreml_model.save('traffic_sign_with_class_weights.mlmodel')
里面具体的参数意义可以参考我的Tensorflow移动端模型转换。
正确转换之后我们就得到CoreML下可用的深度学习模型了,剩下的只需要在IOS工程中正确调用便可,稍微有些IOS 开发相关的知识就能完成。
有需要的童鞋可以关注下这个github list:
https://github.com/likedan/Awesome-CoreML-Models
里面有很多CoreML相关的Demo,可以用来进行二次开发。
这里我提供一个分类Demo,具体工程在我的Github上:
https://github.com/ItchyHiker/Traffic_Sign_Classification_Keras_CoreML/tree/master/ClassifyingImagesWithVisionAndCoreML
最后的结果, 分类的label按照训练数据的子类文件夹排序:
至此我们就完成一个深度学习应用的开发了,这里只是抛砖引玉,要实现其他功能的应用,流程也大致如此。
所有的工程文件都在我的github上:
https://github.com/ItchyHiker/Traffic_Sign_Classification_Keras_CoreML
希望这篇文章可以对入门计算机视觉的童鞋有所裨益,有什么问题都可以留言或者私信讨论。
Todo:
完成ncnn 调用的 demo