Self-defined Models
Self-defined models (and data sets) can be incorporated into PocketFlow by implementing a new ModelHelper
class. The ModelHelper
class includes the definition of data input pipeline as well as the network's forward pass and loss function. With the self-defined ModelHelper
, the network can be either trained without any constraints using FullPrecLearner
, or trained with certain model compression algorithms using other learners, e.g. ChannelPrunedLearner
for channel pruning or UniformQuantTFLearner
for uniform quantization. In this tutorial, we will define a 4-layer convolutional neural network (2 conv. layers + 2 dense layers) for image classification on the Fashion-MNIST data set under the PocketFlow framework. Afterwards, we shall demonstrate how to train this self-defined model with different model compression components.
The Essentials
To use self-defined models and data sets in PocketFlow, we need to provide the following two items in advance to describe the overall training workflow:
- Data Input Pipeline: this tells PocketFlow how to parse features and ground-truth labels from data files.
- Network Definition: this tells PocketFlow how to compute the network's predictions and loss function's value.
The ModelHelper
class, which is a sub-class of the abstract base class AbstractModelHelper
, is designed to provide such definitions. In PocketFlow, we have offered several ModelHelper
classes to describe different combinations of data sets and model architectures. To use self-defined models, a new ModelHelper
class should be implemented. Besides, we need an execution script to call this newly defined ModelHelper
class.
P.S.: You can find the full code used in this tutorial under the "./examples" directory.
Data Input Pipeline
To start with, we need to tell PocketFlow how data files should be parsed. Here, we define a class named FMnistDataset
to create iterators over the Fashion-MNIST training and test subsets. Every time the iterator is called, it will return a mini-batch of images and corresponding ground-truth labels.
Below is the full implementation of FMnistDataset
class (this should be placed under the "./datasets" directory, named as "fmnist_dataset.py"):
import os
import gzip
import numpy as np
import tensorflow as tf
from datasets.abstract_dataset import AbstractDataset
FLAGS = tf.app.flags.FLAGS
tf.app.flags.DEFINE_integer('nb_classes', 10, '# of classes')
tf.app.flags.DEFINE_integer('nb_smpls_train', 60000, '# of samples for training')
tf.app.flags.DEFINE_integer('nb_smpls_val', 5000, '# of samples for validation')
tf.app.flags.DEFINE_integer('nb_smpls_eval', 10000, '# of samples for evaluation')
tf.app.flags.DEFINE_integer('batch_size', 128, 'batch size per GPU for training')
tf.app.flags.DEFINE_integer('batch_size_eval', 100, 'batch size for evaluation')
# Fashion-MNIST specifications
IMAGE_HEI = 28
IMAGE_WID = 28
IMAGE_CHN = 1
def load_mnist(image_file, label_file):
"""Load images and labels from *.gz files.
This function is modified from utils/mnist_reader.py in the Fashion-MNIST repo.
Args:
* image_file: file path to images
* label_file: file path to labels
Returns:
* images: np.array of the image data
* labels: np.array of the label data
"""
with gzip.open(label_file, 'rb') as i_file:
labels = np.frombuffer(i_file.read(), dtype=np.uint8, offset=8)
with gzip.open(image_file, 'rb') as i_file:
images = np.frombuffer(i_file.read(), dtype=np.uint8, offset=16)
image_size = IMAGE_HEI * IMAGE_WID * IMAGE_CHN
assert images.size == image_size * len(labels)
images = images.reshape(len(labels), image_size)
return images, labels
def parse_fn(image, label, is_train):
"""Parse an (image, label) pair and apply data augmentation if needed.
Args:
* image: image tensor
* label: label tensor
* is_train: whether data augmentation should be applied
Returns:
* image: image tensor
* label: one-hot label tensor
"""
# data parsing
label = tf.one_hot(tf.reshape(label, []), FLAGS.nb_classes)
image = tf.cast(tf.reshape(image, [IMAGE_HEI, IMAGE_WID, IMAGE_CHN]), tf.float32)
image = tf.image.per_image_standardization(image)
# data augmentation
if is_train:
image = tf.image.resize_image_with_crop_or_pad(image, IMAGE_HEI + 8, IMAGE_WID + 8)
image = tf.random_crop(image, [IMAGE_HEI, IMAGE_WID, IMAGE_CHN])
image = tf.image.random_flip_left_right(image)
return image, label
class FMnistDataset(AbstractDataset):
'''Fashion-MNIST dataset.'''
def __init__(self, is_train):
"""Constructor function.
Args:
* is_train: whether to construct the training subset
"""
# initialize the base class
super(FMnistDataset, self).__init__(is_train)
# choose local files or HDFS files w.r.t. FLAGS.data_disk
if FLAGS.data_disk == 'local':
assert FLAGS.data_dir_local is not None, '<FLAGS.data_dir_local> must not be None'
data_dir = FLAGS.data_dir_local
elif FLAGS.data_disk == 'hdfs':
assert FLAGS.data_hdfs_host is not None and FLAGS.data_dir_hdfs is not None, \
'both <FLAGS.data_hdfs_host> and <FLAGS.data_dir_hdfs> must not be None'
data_dir = FLAGS.data_hdfs_host + FLAGS.data_dir_hdfs
else:
raise ValueError('unrecognized data disk: ' + FLAGS.data_disk)
# setup paths to image & label files, and read in images & labels
if is_train:
self.batch_size = FLAGS.batch_size
image_file = os.path.join(data_dir, 'train-images-idx3-ubyte.gz')
label_file = os.path.join(data_dir, 'train-labels-idx1-ubyte.gz')
else:
self.batch_size = FLAGS.batch_size_eval
image_file = os.path.join(data_dir, 't10k-images-idx3-ubyte.gz')
label_file = os.path.join(data_dir, 't10k-labels-idx1-ubyte.gz')
self.images, self.labels = load_mnist(image_file, label_file)
self.parse_fn = lambda x, y: parse_fn(x, y, is_train)
def build(self, enbl_trn_val_split=False):
"""Build iterator(s) for tf.data.Dataset() object.
Args:
* enbl_trn_val_split: whether to split into training & validation subsets
Returns:
* iterator_trn: iterator for the training subset
* iterator_val: iterator for the validation subset
OR
* iterator: iterator for the chosen subset (training OR testing)
"""
# create a tf.data.Dataset() object from NumPy arrays
dataset = tf.data.Dataset.from_tensor_slices((self.images, self.labels))
dataset = dataset.map(self.parse_fn, num_parallel_calls=FLAGS.nb_threads)
# create iterators for training & validation subsets separately
if self.is_train and enbl_trn_val_split:
iterator_val = self.__make_iterator(dataset.take(FLAGS.nb_smpls_val))
iterator_trn = self.__make_iterator(dataset.skip(FLAGS.nb_smpls_val))
return iterator_trn, iterator_val
return self.__make_iterator(dataset)
def __make_iterator(self, dataset):
"""Make an iterator from tf.data.Dataset.
Args:
* dataset: tf.data.Dataset object
Returns:
* iterator: iterator for the dataset
"""
dataset = dataset.apply(tf.contrib.data.shuffle_and_repeat(buffer_size=FLAGS.buffer_size))
dataset = dataset.batch(self.batch_size)
dataset = dataset.prefetch(FLAGS.prefetch_size)
iterator = dataset.make_one_shot_iterator()
return iterator
When creating an object of FMnistDataset
class, an extra argument named is_train
should be provided to toggle between the training and test subsets. The data files can be either store on the local machine or the HDFS cluster, and the directory path is specified in the path configuration file, e.g.:
data_dir_local_fmnist = /home/user_name/datasets/Fashion-MNIST
The constructor function loads images and labels from *.gz files, each stored in a NumPy array. The build
function is then used to create a TensorFlow's data set iterator from these two NumPy arrays. Particularly, if both enbl_trn_val_split
and is_train
are True, then the original training subset will be divided into two parts, one for model training and the other for validation.
Network Definition
Now we implement a new ModelHelper
class to utilize the above data input pipeline to define the network's training workflow. Below is the full implementation of ModelHelper
class (this should be placed under the "./nets" directory, named as "convnet_at_fmnist.py"):
import tensorflow as tf
from nets.abstract_model_helper import AbstractModelHelper
from datasets.fmnist_dataset import FMnistDataset
from utils.lrn_rate_utils import setup_lrn_rate_piecewise_constant
from utils.multi_gpu_wrapper import MultiGpuWrapper as mgw
FLAGS = tf.app.flags.FLAGS
tf.app.flags.DEFINE_float('nb_epochs_rat', 1.0, '# of training epochs\' ratio')
tf.app.flags.DEFINE_float('lrn_rate_init', 1e-1, 'initial learning rate')
tf.app.flags.DEFINE_float('batch_size_norm', 128, 'normalization factor of batch size')
tf.app.flags.DEFINE_float('momentum', 0.9, 'momentum coefficient')
tf.app.flags.DEFINE_float('loss_w_dcy', 3e-4, 'weight decaying loss\'s coefficient')
def forward_fn(inputs, data_format):
"""Forward pass function.
Args:
* inputs: inputs to the network's forward pass
* data_format: data format ('channels_last' OR 'channels_first')
Returns:
* inputs: outputs from the network's forward pass
"""
# transpose the image tensor if needed
if data_format == 'channel_first':
inputs = tf.transpose(inputs, [0, 3, 1, 2])
# conv1
inputs = tf.layers.conv2d(inputs, 32, [5, 5], padding='same',
data_format=data_format, activation=tf.nn.relu, name='conv1')
inputs = tf.layers.max_pooling2d(inputs, [2, 2], 2, data_format=data_format, name='pool1')
# conv2
inputs = tf.layers.conv2d(inputs, 64, [5, 5], padding='same',
data_format=data_format, activation=tf.nn.relu, name='conv2')
inputs = tf.layers.max_pooling2d(inputs, [2, 2], 2, data_format=data_format, name='pool2')
# fc3
inputs = tf.layers.flatten(inputs, name='flatten')
inputs = tf.layers.dense(inputs, 1024, activation=tf.nn.relu, name='fc3')
# fc4
inputs = tf.layers.dense(inputs, FLAGS.nb_classes, name='fc4')
inputs = tf.nn.softmax(inputs, name='softmax')
return inputs
class ModelHelper(AbstractModelHelper):
"""Model helper for creating a ConvNet model for the Fashion-MNIST dataset."""
def __init__(self):
"""Constructor function."""
# class-independent initialization
super(ModelHelper, self).__init__()
# initialize training & evaluation subsets
self.dataset_train = FMnistDataset(is_train=True)
self.dataset_eval = FMnistDataset(is_train=False)
def build_dataset_train(self, enbl_trn_val_split=False):
"""Build the data subset for training, usually with data augmentation."""
return self.dataset_train.build(enbl_trn_val_split)
def build_dataset_eval(self):
"""Build the data subset for evaluation, usually without data augmentation."""
return self.dataset_eval.build()
def forward_train(self, inputs, data_format='channels_last'):
"""Forward computation at training."""
return forward_fn(inputs, data_format)
def forward_eval(self, inputs, data_format='channels_last'):
"""Forward computation at evaluation."""
return forward_fn(inputs, data_format)
def calc_loss(self, labels, outputs, trainable_vars):
"""Calculate loss (and some extra evaluation metrics)."""
loss = tf.losses.softmax_cross_entropy(labels, outputs)
loss += FLAGS.loss_w_dcy * tf.add_n([tf.nn.l2_loss(var) for var in trainable_vars])
accuracy = tf.reduce_mean(
tf.cast(tf.equal(tf.argmax(labels, axis=1), tf.argmax(outputs, axis=1)), tf.float32))
metrics = {'accuracy': accuracy}
return loss, metrics
def setup_lrn_rate(self, global_step):
"""Setup the learning rate (and number of training iterations)."""
nb_epochs = 160
idxs_epoch = [40, 80, 120]
decay_rates = [1.0, 0.1, 0.01, 0.001]
batch_size = FLAGS.batch_size * (1 if not FLAGS.enbl_multi_gpu else mgw.size())
lrn_rate = setup_lrn_rate_piecewise_constant(global_step, batch_size, idxs_epoch, decay_rates)
nb_iters = int(FLAGS.nb_smpls_train * nb_epochs * FLAGS.nb_epochs_rat / batch_size)
return lrn_rate, nb_iters
@property
def model_name(self):
"""Model's name."""
return 'convnet'
@property
def dataset_name(self):
"""Dataset's name."""
return 'fmnist'
In the build_dataset_train
and build_dataset_eval
functions, we adopt the previously introduced FMnistDataset
class to define the data input pipeline. The network forward-pass computation is defined in the forward_train
and forward_eval
functions, which corresponds to the training and evaluation graph, respectively. The training graph is slightly different from evaluation graph, such as operations related to the batch normalization layers. The calc_loss
function calculates the loss function's value and extra evaluation metrics, e.g. classification accuracy. Finally, the setup_lrn_rate
function defines the learning rate schedule, as well as how many training iterations are need.
Execution Script
Besides the self-defined ModelHelper
class, we still need an execution script to pass it to the corresponding model compression component to start the training process. Below is the full implementation (this should be placed under the "./nets" directory, named as "convnet_at_fmnist_run.py"):
import traceback
import tensorflow as tf
from nets.convnet_at_fmnist import ModelHelper
from learners.learner_utils import create_learner
FLAGS = tf.app.flags.FLAGS
tf.app.flags.DEFINE_string('log_dir', './logs', 'logging directory')
tf.app.flags.DEFINE_boolean('enbl_multi_gpu', False, 'enable multi-GPU training')
tf.app.flags.DEFINE_string('learner', 'full-prec', 'learner\'s name')
tf.app.flags.DEFINE_string('exec_mode', 'train', 'execution mode: train / eval')
tf.app.flags.DEFINE_boolean('debug', False, 'debugging information')
def main(unused_argv):
"""Main entry."""
try:
# setup the TF logging routine
if FLAGS.debug:
tf.logging.set_verbosity(tf.logging.DEBUG)
else:
tf.logging.set_verbosity(tf.logging.INFO)
sm_writer = tf.summary.FileWriter(FLAGS.log_dir)
# display FLAGS's values
tf.logging.info('FLAGS:')
for key, value in FLAGS.flag_values_dict().items():
tf.logging.info('{}: {}'.format(key, value))
# build the model helper & learner
model_helper = ModelHelper()
learner = create_learner(sm_writer, model_helper)
# execute the learner
if FLAGS.exec_mode == 'train':
learner.train()
elif FLAGS.exec_mode == 'eval':
learner.download_model()
learner.evaluate()
else:
raise ValueError('unrecognized execution mode: ' + FLAGS.exec_mode)
# exit normally
return 0
except ValueError:
traceback.print_exc()
return 1 # exit with errors
if __name__ == '__main__':
tf.app.run()
Network Training with PocketFlow
To train the self-defined model without any constraint, use FullPrecLearner
:
$ ./scripts/run_local.sh nets/convnet_at_fmnist_run.py \
--learner full-prec
To train the self-defined model with the uniform quantization constraint, use UniformQuantTFLearner
:
$ ./scripts/run_local.sh nets/convnet_at_fmnist_run.py \
--learner uniform-tf