Keras Basic Usage
Keras是一个基于Python的深度学习库,是一个高级的神经网络API,可以运行在Tensorflow, CNTK, Theano等深度学习框架的顶层。对于研究者来说,使用Keras可以快速开发和构建深度学习模型。
tf.keras是Google根据Keras API进行的Tensorflow版本的实现。官网上给出了一个非常简短的例子来介绍Keras的使用方法,我们来看一下:
首先check一下tensorflow和keras的版本
Python 3.6.8 |Anaconda, Inc.| (default, Dec 29 2018, 19:04:46)
[GCC 4.2.1 Compatible Clang 4.0.1 (tags/RELEASE_401/final)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import tensorflow as tf
>>> tf.__version__
'1.12.0'
>>> tf.keras.__version__
'2.1.6-tf'
接下来我们运行一下TF官网上给出的Demo
from __future__ import absolute_import, division, print_function, unicode_literals
import tensorflow as tf
# Load mnist dataset
mnist = tf.keras.datasets.mnist
# 获取训练集和测试集
(x_train, y_train), (x_test, y_test) = mnist.load_data()
# 数据预处理,此处利用到了归一化
x_train, x_test = x_train / 255.0, x_test / 255.0
# 搭建神经网络
model = tf.keras.models.Sequential([
tf.keras.layers.Flatten(input_shape=(28, 28)),
tf.keras.layers.Dense(128, activation='relu'),
tf.keras.layers.Dropout(0.2),
tf.keras.layers.Dense(10, activation='softmax')
])
# 选择优化器,损失函数和模型度量方法
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
# 模型拟合
model.fit(x_train, y_train, epochs=5)
# 模型评价
model.evaluate(x_test, y_test)
如果读者之前没有运行过这段代码,那么首先这段代码会下载mnist,之后便是模型训练的结果。
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz
11493376/11490434 [==============================] - 152s 13us/step
Epoch 1/5
2019-07-13 21:38:59.927952: I tensorflow/core/platform/cpu_feature_guard.cc:141] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 FMA
60000/60000 [==============================] - 5s 77us/step - loss: 0.3011 - acc: 0.9134
Epoch 2/5
60000/60000 [==============================] - 4s 60us/step - loss: 0.1430 - acc: 0.9581
Epoch 3/5
60000/60000 [==============================] - 4s 61us/step - loss: 0.1068 - acc: 0.9673
Epoch 4/5
60000/60000 [==============================] - 4s 61us/step - loss: 0.0868 - acc: 0.9739
Epoch 5/5
60000/60000 [==============================] - 4s 63us/step - loss: 0.0749 - acc: 0.9769
10000/10000 [==============================] - 0s 29us/step
[0.07364166865674779, 0.9761]
可以看到,模型在训练了5个epoch之后,就达到了0.9769的精度,在验证集上的精度也达到了0.9761的精度。我们搭建了一个简易的带有Dropout功能的全连接网络,就能在mnist数据集上达到不俗的精度。而且,上面这段代码并不难理解。
显然地,对于演示Demo来讲,这段代码确实简单。但是在实际项目生产中,有很多需要注意的点,本文将从以下几个部分来说明Keras的使用方法与需要注意的点。主要是:1. 版本问题;2. 数据加载;3. 模型构建;4. 损失函数,优化器,度量方法选择;5. 模型拟合。
版本问题
TF是公认的向下兼容性比较差,这也就意味着高版本的代码大多数情况下无法在低版本的TF框架下运行,或多或少地都需要改变一些代码的细节,这个问题一直被TF的使用者所诟病。所以,在使用TF前,读者应该要仔细Check框架版本,以免到时候出现一些莫名其妙的问题。
如果需要使用GPU,除了CheckTF的版本之外,还需要注意计算机上所安装的CUDA版本与cuDNN的版本。以下是tf-gpu的版本所对应的CUDA和cuDNN版本。更多细节可查看https://www.tensorflow.org/install/source#linux
。
查看TF和Keras版本的方法我们在上面已经给出了,下面给出查看本机CUDA和cuDNN版本的方法。如果没有安装CUDA,则下面的命令是无效的。
# 一般来说,CUDA会安装在/usr/local文件夹下面
$ cat /usr/local/cuda/version.txt
# CUDA Version 9.0.176
# CUDA Patch Version 9.0.176.1
# CUDA Patch Version 9.0.176.2
# CUDA Patch Version 9.0.176.3
# CUDA Patch Version 9.0.176.4
# 查看cuDNN版本
$ cat /usr/local/cuda/include/cudnn.h | grep CUDNN_MAJOR -A 2
# #define CUDNN_MAJOR 7
# #define CUDNN_MINOR 5
# #define CUDNN_PATCHLEVEL 0
# --
# define CUDNN_VERSION (CUDNN_MAJOR * 1000 + CUDNN_MINOR * 100 + CUDNN_PATCHLEVEL)
# #include "driver_types.h"
确保TF-GPU支持CUDA和cuDNN版本之后,我们就可以进行模型编写与训练工作。当然,如果读者不需要使用GPU,那么只需CheckTF的版本即可。
数据加载
在TF官网上给出的Demo里面,我们看到数据加载就只有一行mnist.load_data(),实际上这只是对mnist数据集进行的一个封装。在实际应用中,数据来源可能是多种多样的,这里以Figaro1k
发型分类数据为例,说明数据构建和加载的基本流程。在后续,笔者会给出适用于更多形式和更多场景的数据构建和加载方式。
对图片分类问题而言,Keras的数据加载方式比原生TF简单太多。读者只需要两步操作就可以加载图片。
Step 1. 构建图片数据的生成器
from functools import partial
from keras_preprocessing.image import ImageDataGenerator
train_datagen = ImageDataGenerator(preprocessing_function=partial(resnet50.preprocess_input, data_format='channels_last'),
rotation_range=20,
width_shift_range=0.2,
height_shift_range=0.2,
horizontal_flip=True,
vertical_flip=True,
featurewise_center=False,
samplewise_center=False,
featurewise_std_normalization=False,
samplewise_std_normalization=False,
zca_whitening=False,
zca_epsilon=1e-6,
brightness_range=None,
shear_range=0.,
zoom_range=0.,
channel_shift_range=0.,
fill_mode='nearest',
cval=0.,
rescale=None,
data_format='channels_last',
validation_split=0.0,
interpolation_order=1,
dtype='float32'
)
我们看到,该段代码建立了一个ImageDataGenerator对象,这个对象有非常多的参数,大部分的参数是图片在训练前的一些预处理工作,比如图片增强,归一化等等,其中preprocessing_function
参数是用户指定的图片预处理方法。
Step 2. 指定数据读取方式
train_generator = train_datagen.flow_from_directory(directory=train_data_path,
target_size=(224, 224),
color_mode='rgb',
classes=None,
class_mode='categorical',
batch_size=batch_size,
shuffle=True,
seed=None,
save_to_dir=None,
save_prefix='',
save_format='jpg',
follow_links=False,
subset=None,
interpolation='nearest')
上述代码指定数据从哪里获取,flow_from_directory
指定数据是从文件夹中获取的,其中directory
参数指定文件夹的路径,需要注意的是,在图片分类问题中,这个函数会自动的搜索train_data_path
下的子文件夹,并认为不同的子文件夹下的图片属于不同的类。
上图表示了图片文件的存储结构,读者在使用这个函数进行图片分类的时候,只需要将图片按照上述方式组织即可。通过上述两行代码,我们就构建好了训练数据的生成器。验证数据的构建过程和上述过程类似,但需要注意的是,验证过程是不需要对图片进行增强的。
所以验证数据一般通过如下手段进行加载。
val_datagen = ImageDataGenerator(preprocessing_function=partial(resnet50.preprocess_input, data_format='channels_last'), featurewise_center=False,
samplewise_center=False,
featurewise_std_normalization=False,
samplewise_std_normalization=False,
zca_whitening=False,
zca_epsilon=1e-6,
rotation_range=0,
width_shift_range=0.,
height_shift_range=0.,
brightness_range=None,
shear_range=0.,
zoom_range=0.,
channel_shift_range=0.,
fill_mode='nearest',
cval=0.,
horizontal_flip=False,
vertical_flip=False,
rescale=None,
validation_split=0.0,
interpolation_order=1,
dtype='float32')
val_generator = val_datagen.flow_from_directory(directory=val_data_path,
target_size=(224, 224),
color_mode='rgb',
classes=None,
class_mode='categorical',
batch_size=batch_size,
shuffle=True,
seed=None,
save_to_dir=None,
save_prefix='',
save_format='jpg',
follow_links=False,
subset=None,
interpolation='nearest')
可以看到,验证阶段是不需要对图片进行增强操作的,但仍需注意的是,验证集的图片同样需要进行预处理,以保证和训练集相同的输入形式,比如归一化等操作。验证所用的图片存储结构和训练所用的图片存储结构是一样的。
读者可根据自己的需要,来调整对输入图片的一些额外的操作。
模型构建
我们以Keras预训练的ResNet50为例,Keras的模型构建同样比原生TF要简单许多。如下所示:
# 加载ResNet50预训练模型,注意,我们移除掉了ResNet50的顶层。include_top=False
model = tf.keras.applications.ResNet50(include_top=False,
weights='imagenet',
input_tensor=None,
input_shape=(224, 224, 3),
pooling=None)
# 设置ResNet前面若干层不可被训练
for layer in model.layers[:-5]:
layer.trainable = False
# Add output layer
new_model = models.Sequential()
new_model.add(model)
# 添加顶层以适应我们的分类问题
new_model.add(layers.Conv2D(filters=1024,
kernel_size=(7, 7),
strides=(1, 1),
padding='valid',
data_format=None,
dilation_rate=(1, 1),
activation=None,
use_bias=True,
kernel_initializer='glorot_uniform',
bias_initializer='zeros',
kernel_regularizer=None,
bias_regularizer=None,
activity_regularizer=None,
kernel_constraint=None,
bias_constraint=None))
new_model.add(layers.Flatten())
new_model.add(layers.Dense(units=1024,
activation=None,
use_bias=True,
kernel_initializer='glorot_uniform',
bias_initializer='zeros',
kernel_regularizer=None,
bias_regularizer=None,
activity_regularizer=None,
kernel_constraint=None,
bias_constraint=None))
new_model.add(layers.Activation(tf.nn.relu))
new_model.add(layers.Dropout(rate=0.5,
noise_shape=None,
seed=None))
new_model.add(layers.Dense(units=7,
activation=None,
use_bias=True,
kernel_initializer='glorot_uniform',
bias_initializer='zeros',
kernel_regularizer=None,
bias_regularizer=None,
activity_regularizer=None,
kernel_constraint=None,
bias_constraint=None))
new_model.add(layers.Activation(tf.nn.softmax))
我们看到,首先加载了ResNet50预训练模型,并去除顶层,然后在根据需要添加我们自己的层。由于Figaro1k
有7个类别,所以最后模型的输出层有7个输出单元,并进行了softmax操作。
损失函数,优化器,评价方法选择
这一步整合在了模型编译步骤里面,只需下面一行代码。
new_model.compile(loss='categorical_crossentropy',
optimizer=optimizers.Adam(lr=1e-4),
metrics=['acc'])
其中,loss指定损失函数,optimizer指定优化器,metrics指定模型评价方法。
模型拟合与验证
这一步也只需要一行代码,如下所示:
history = new_model.fit_generator(generator=train_generator,
steps_per_epoch=math.ceil(n_train_samples / batch_size),
epochs=num_epochs,
verbose=1,
callbacks=[tensorboard_callback, lr_callback],
validation_data=val_generator,
validation_steps=math.ceil(n_test_samples / batch_size),
class_weight=None,
max_queue_size=10,
workers=1,
use_multiprocessing=False,
shuffle=True,
initial_epoch=0)
fit_generator
方法将启动模型的训练过程与验证过程,其中generator=train_generator
将训练集生成器喂给模型,validation_data=val_generator
指定验证集生成器,callbacks=[tensorboard_callback, lr_callback]
指定回调函数,这里我们指定了两个回调,一个是tensorboard,一个是学习率衰减的回调。学习率衰减的回调函数如下:
def lr_schedule(epoch, learning_rate):
"""
Returns a custom learning rate that decreases as epochs progress.
"""
# learning_rate = 1e-4
if epoch > 5:
learning_rate = learning_rate * 0.5
if epoch > 10:
learning_rate = learning_rate * 0.5
if epoch > 15:
learning_rate = learning_rate * 0.5
if epoch > 20:
learning_rate = learning_rate * 0.5
if epoch > 25:
learning_rate = learning_rate * 0.5
if epoch > 30:
learning_rate = learning_rate * 0.5
if epoch > 35:
learning_rate = learning_rate * 0.5
if epoch > 40:
learning_rate = learning_rate * 0.5
# tf.summary.scalar('learning rate', learning_rate)
return learning_rate
lr_callback = keras.callbacks.LearningRateScheduler(schedule=lr_schedule)
应用学习率衰减的回调后,模型在训练过程会按照用户所定义的学习率衰减的schedule进行。
Tensorboard的回调方法如下:
tensorboard_callback = keras.callbacks.TensorBoard(log_dir=logdir,
histogram_freq=0,
batch_size=32,
write_graph=True,
write_grads=False,
write_images=False,
embeddings_freq=0,
embeddings_layer_names=None,
embeddings_metadata=None,
embeddings_data=None)
非常遗憾的是,fit_generator
目前对tensorboard的回调仅支持标量,但不支持histogram等向量的写入,相关issues:https://stackoverflow.com/questions/42112260/how-do-i-use-the-tensorboard-callback-of-keras/42112935
,https://github.com/keras-team/keras/issues/3358#issuecomment-312531958
。
在代码运行之后,读者可以利用
$ tensorboard --logdir=logdir
来访问Tensorboard的输出结果。
以上是Keras的基本使用过程,后续笔者会给出更多的关于Keras的细节。
完整的代码
以下为完整的代码:
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import os
# 指定GPU
os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"
os.environ["CUDA_VISIBLE_DEVICES"] = '0'
from datetime import datetime
import math
import tensorflow as tf
from tensorflow.python import keras
from tensorflow.python.keras import backend as K
from keras_preprocessing.image import ImageDataGenerator
from keras_applications import resnet50
models = keras.models
layers = keras.layers
optimizers = keras.optimizers
# set GPU memory
if 'tensorflow' == K.backend():
config = tf.ConfigProto()
config.gpu_options.allow_growth = True
sess = tf.Session(config=config)
import config
from functools import partial
logdir = "/home/wuyanxue/PyCharmProjects/hairstyle_classification/logs/scalars/" + datetime.now().strftime("%Y%m%d-%H%M%S")
tensorboard_callback = keras.callbacks.TensorBoard(log_dir=logdir,
histogram_freq=0,
batch_size=32,
write_graph=True,
write_grads=False,
write_images=False,
embeddings_freq=0,
embeddings_layer_names=None,
embeddings_metadata=None,
embeddings_data=None)
# Define learning rate decay
def lr_schedule(epoch, learning_rate):
"""
Returns a custom learning rate that decreases as epochs progress.
"""
# learning_rate = 1e-4
if epoch > 5:
learning_rate = learning_rate * 0.5
if epoch > 10:
learning_rate = learning_rate * 0.5
if epoch > 15:
learning_rate = learning_rate * 0.5
if epoch > 20:
learning_rate = learning_rate * 0.5
if epoch > 25:
learning_rate = learning_rate * 0.5
if epoch > 30:
learning_rate = learning_rate * 0.5
if epoch > 35:
learning_rate = learning_rate * 0.5
if epoch > 40:
learning_rate = learning_rate * 0.5
# tf.summary.scalar('learning rate', learning_rate)
return learning_rate
lr_callback = keras.callbacks.LearningRateScheduler(schedule=lr_schedule)
# Param
num_epochs = 50
batch_size = 32
n_train_samples = 840
n_test_samples = 210
# Data
train_datagen = ImageDataGenerator(preprocessing_function=partial(resnet50.preprocess_input, data_format='channels_last'),
rotation_range=20,
width_shift_range=0.2,
height_shift_range=0.2,
horizontal_flip=True,
vertical_flip=True,
featurewise_center=False,
samplewise_center=False,
featurewise_std_normalization=False,
samplewise_std_normalization=False,
zca_whitening=False,
zca_epsilon=1e-6,
brightness_range=None,
shear_range=0.,
zoom_range=0.,
channel_shift_range=0.,
fill_mode='nearest',
cval=0.,
rescale=None,
data_format='channels_last',
validation_split=0.0,
interpolation_order=1,
dtype='float32'
)
train_generator = train_datagen.flow_from_directory(directory=config.figaro1k_train_path,
target_size=(224, 224),
color_mode='rgb',
classes=None,
class_mode='categorical',
batch_size=batch_size,
shuffle=True,
seed=None,
save_to_dir=None,
save_prefix='',
save_format='jpg',
follow_links=False,
subset=None,
interpolation='nearest')
val_datagen = ImageDataGenerator(preprocessing_function=partial(resnet50.preprocess_input, data_format='channels_last'))
val_generator = val_datagen.flow_from_directory(directory=config.figaro1k_test_path,
target_size=(224, 224),
color_mode='rgb',
classes=None,
class_mode='categorical',
batch_size=batch_size,
shuffle=True,
seed=None,
save_to_dir=None,
save_prefix='',
save_format='jpg',
follow_links=False,
subset=None,
interpolation='nearest')
# Model
model = tf.keras.applications.ResNet50(include_top=False,
weights='imagenet',
input_tensor=None,
input_shape=(224, 224, 3),
pooling=None)
for layer in model.layers[:-5]:
layer.trainable = False
# Add output layer
new_model = models.Sequential()
new_model.add(model)
new_model.add(layers.Conv2D(filters=1024,
kernel_size=(7, 7),
strides=(1, 1),
padding='valid',
data_format=None,
dilation_rate=(1, 1),
activation=None,
use_bias=True,
kernel_initializer='glorot_uniform',
bias_initializer='zeros',
kernel_regularizer=None,
bias_regularizer=None,
activity_regularizer=None,
kernel_constraint=None,
bias_constraint=None))
new_model.add(layers.Flatten())
new_model.add(layers.Dense(units=1024,
activation=None,
use_bias=True,
kernel_initializer='glorot_uniform',
bias_initializer='zeros',
kernel_regularizer=None,
bias_regularizer=None,
activity_regularizer=None,
kernel_constraint=None,
bias_constraint=None))
new_model.add(layers.Activation(tf.nn.relu))
new_model.add(layers.Dropout(rate=0.5,
noise_shape=None,
seed=None))
new_model.add(layers.Dense(units=7,
activation=None,
use_bias=True,
kernel_initializer='glorot_uniform',
bias_initializer='zeros',
kernel_regularizer=None,
bias_regularizer=None,
activity_regularizer=None,
kernel_constraint=None,
bias_constraint=None))
new_model.add(layers.Activation(tf.nn.softmax))
# new_model.summary(line_length=None,
# positions=None,
# print_fn=None)
# Compile
new_model.compile(loss='categorical_crossentropy',
optimizer=optimizers.Adam(lr=1e-4),
metrics=['acc'])
history = new_model.fit_generator(generator=train_generator,
steps_per_epoch=math.ceil(n_train_samples / batch_size),
epochs=num_epochs,
verbose=1,
callbacks=[tensorboard_callback, lr_callback],
validation_data=val_generator,
validation_steps=math.ceil(n_test_samples / batch_size),
class_weight=None,
max_queue_size=10,
workers=1,
use_multiprocessing=False,
shuffle=True,
initial_epoch=0)