项目作者: musket-ml

项目描述 :
Puny attempt to build reusable training pipeline for image classification
高级语言: Python
项目地址: git://github.com/musket-ml/classification_training_pipeline.git
创建时间: 2018-11-27T14:51:04Z
项目社区:https://github.com/musket-ml/classification_training_pipeline

开源协议:MIT License

下载


Classification training pipeline

Build status

Classification pipeline is a declarative pipeline for single and multi output image classification tasks.
It can be also used for regression tasks.

This package is a part of Musket ML framework.

Reasons to use Classification Pipeline

Classification Pipeline was developed with a focus of enabling to make fast and
simply-declared experiments, which can be easily stored,
reproduced and compared to each other.

Classification Pipeline has a lot of common parts with Generic pipeline, but it is easier to define an architecture of the network.
Also there are a number of classification-specific features.

The pipeline provides the following features:

  • Allows to describe experiments in a compact and expressive way
  • Provides a way to store and compare experiments in order to methodically find the best deap learning solution
  • Easy to share experiments and their results to work in a team
  • Experiment configurations are separated from model definitions
  • It is easy to configure network architecture
  • Provides great flexibility and extensibility via support of custom substances
  • Common blocks like an architecture, callbacks, model metrics, predictions vizualizers and others should be written once and be a part of a common library

Installation

  1. pip install classification_pipeline

Note: this package requires python 3.6

This package is a part of Musket ML framework,
it is recommended to install the whole collection of the framework
packages at once using instructions here.

Launching

Launching experiments

fit.py script is designed to launch experiment training.

In order to run the experiment or a number of experiments,

A typical command line may look like this:

musket fit --project "path/to/project" --name "experiment_name" --num_gpus=1 --gpus_per_net=1 --num_workers=1 --cache "path/to/cache/folder"

—project points to the root of the project

—name is the name of the project sub-folder containing experiment yaml file.

—num_gpus sets number of GPUs to use during experiment launch.

—gpus_per_net is a maximum number of GPUs to use per single experiment.

—num_workers sets number of workers to use.

—cache points to a cache folder to store the temporary data.

Other parameters can be found in the fit script reference

Launching tasks

task.py script is designed to launch experiment training.

Tasks must be defined in the project python scope and marked by an
annotation like this:

  1. from musket_core import tasks, model
  2. @tasks.task
  3. def measure2(m: model.ConnectedModel):
  4. return result

In order to run the experiment or a number of experiments,

A typical command line may look like this:

python -m musket_core.task --project "path/to/project" --name "experiment_name" --task "task_name" --num_gpus=1 --gpus_per_net=1 --num_workers=1 --cache "path/to/cache/folder"

—project points to the root of the project

—name is the name of the project sub-folder containing experiment yaml file.

—task is the name of the task function.

—num_gpus sets number of GPUs to use during experiment launch.

—gpus_per_net is a maximum number of GPUs to use per single experiment.

—num_workers sets number of workers to use.

—cache points to a cache folder to store the temporary data.

Other parameters can be found in the task script reference

Launching project analysis

analize.py script is designed to launch project-scope analysis.
It is located in the musket_core root folder.

Note that only experiments, which training is already finished will be covered.

musket analize --inputFolder "path/to/project"

—inputFolder points to a folder to search for finished experiments in. Typically, project root.

Other parameters can be found in the analyze script reference

Usage guide

Training a model

Let’s start from a simple example of classification. Suppose, your data are structured as follows: a .cvs file with images ids and their labels and a folder with all these images. For training a neural network to classify these images all you need are few lines of python code:

  1. import musket_core
  2. from classification_pipeline import classification
  3. class ProteinDataGenerator:
  4. def __init__(self, paths, labels):
  5. self.paths, self.labels = paths, labels
  6. def __len__(self):
  7. return len(self.paths)
  8. def __getitem__(self, idx):
  9. X,y = self.__load_image(self.paths[idx]),self.labels[idx]
  10. return PredictionItem(self.paths[idx],X, y)
  11. def __load_image(self, path):
  12. R = Image.open(path + '_red.png')
  13. G = Image.open(path + '_green.png')
  14. B = Image.open(path + '_blue.png')
  15. im = np.stack((
  16. np.array(R),
  17. np.array(G),
  18. np.array(B),
  19. ), -1)
  20. return im
  21. dataset = ProteinDataGenerator(paths,labels)
  22. cfg = classification.parse("config.yaml")
  23. cfg.fit(dataset)

Looks simple, but there is a config.yaml file in the code, and probably it is the place where everything actually happens.

  1. architecture: DenseNet201 #pre-trained model we are going to use
  2. pooling: avg
  3. augmentation: #define some minimal augmentations on images
  4. Fliplr: 0.5
  5. Flipud: 0.5
  6. classes: 28 #define the number of classes
  7. activation: sigmoid #as we have multilabel classification, the activation for last layer is sigmoid
  8. weights: imagenet #we would like to start from network pretrained on imagenet dataset
  9. shape: [224, 224, 3] #our desired input image size, everything will be resized to fit
  10. testSplit: 0.4
  11. optimizer: Adam #Adam optimizer is a good default choice
  12. batch: 16 #our batch size will be 16
  13. lr: 0.005
  14. copyWeights: true
  15. metrics: #we would like to track some metrics
  16. - binary_accuracy
  17. - macro_f1
  18. primary_metric: val_binary_accuracy #the most interesting metric is val_binary_accuracy
  19. primary_metric_mode: max
  20. callbacks: #configure some minimal callbacks
  21. EarlyStopping:
  22. patience: 3
  23. monitor: val_macro_f1
  24. mode: max
  25. verbose: 1
  26. ReduceLROnPlateau:
  27. patience: 2
  28. factor: 0.3
  29. monitor: val_binary_accuracy
  30. mode: max
  31. cooldown: 1
  32. verbose: 1
  33. loss: binary_crossentropy #we use binary_crossentropy loss
  34. stages:
  35. - epochs: 10 #let's go for 100 epochs

So as you see, we have decomposed our task in two parts, code that actually trains the model and experiment configuration,
which determines the model and how it should be trained from the set of predefined building blocks.

Moreover, the whole fitting and prediction process can be launched with built-in script,
the only really required python code is dataset definition to let the system know, which data to load.

What does this code actually do behind the scenes?

  • it splits your data into 5 folds, and trains one model per fold;
  • it takes care of model checkpointing, generates example image/label tuples, collects training metrics. All this data will
    be stored in the folders just near your config.yaml;
  • All your folds are initialized from fixed default seed, so different experiments will use exactly the same train/validation splits.

Also, datasets can be specified directly in your config file in more generic way, see examples ds_1, ds_2, ds_3 in “segmentation_training_pipeline/examples/people” folder. In this case you can just call cfg.fit() without providing dataset programmatically.

Lets discover what’s going on in more details:

General train properties

Lets take our standard example and check the following set of instructions:

  1. testSplit: 0.4
  2. optimizer: Adam #Adam optimizer is a good default choice
  3. batch: 16 #Our batch size will be 16
  4. metrics: #We would like to track some metrics
  5. - binary_accuracy
  6. - iou
  7. primary_metric: val_binary_accuracy #and the most interesting metric is val_binary_accuracy
  8. loss: binary_crossentropy #We use simple binary_crossentropy loss

testSplit Splits the train set into two parts, using one part for train and leaving the other untouched for a later testing.
The split is shuffled.

optimizer sets the optimizer.

batch sets the training batch size.

metrics sets the metrics to track during the training process. Metric calculation results will be printed in the console and to metrics folder of the experiment.

primary_metric Metric to track during the training process. Metric calculation results will be printed in the console and to metrics folder of the experiment.
Besides tracking, this metric will be also used by default for metric-related activity, in example, for decision regarding which epoch results are better.

loss sets the loss function. if your network has multiple outputs, you also may pass a list of loss functions (one per output)

Framework supports composing loss as a weighted sum of predefined loss functions. For example, following construction

  1. loss: binary_crossentropy+0.1*dice_loss

will result in loss function which is composed from binary_crossentropy and dice_loss functions.

There are many more properties to check in Reference of root properties

Defining architecture

Lets take a look at the following part of our example:

  1. architecture: DenseNet201 #let's select architecture that we would like to use
  2. classes: 28 #we have just one class (mask or no mask)
  3. activation: sigmoid #one class means that our last layer should use sigmoid activation
  4. weights: imagenet
  5. shape: [224, 224, 3] #This is our desired input image and mask size, everything will be resized to fit.

The following three properties are required to set:

architecture This property configures architecture that should be used. net, Linknet, PSP, FPN and more are supported.

classes sets the number of classes that should be used.

The following ones are optional, but commonly used:

activation sets activation function that should be used in last layer.

shape set the desired shape of the input picture and mask, in the form heigth, width, number of channels. Input will be resized to fit.

weights configures initial weights of the encoder.

Image Augmentations

Framework uses awesome imgaug library for augmentation, so you only need to configure your augmentation process in declarative way like in the following example:

  1. augmentation:
  2. Fliplr: 0.5
  3. Flipud: 0.5
  4. Affine:
  5. scale: [0.8, 1.5] #random scalings
  6. translate_percent:
  7. x: [-0.2,0.2] #random shifts
  8. y: [-0.2,0.2]
  9. rotate: [-16, 16] #random rotations on -16,16 degrees
  10. shear: [-16, 16] #random shears on -16,16 degrees

augmentation property defines IMGAUG transformations sequence.
Each object is mapped on IMGAUG transformer by name, parameters are mapped too.

In this example, Fliplr and Flipud keys are automatically mapped on Flip agugmenters,
their 0.5 parameter is mapped on the first p parameter of the augmenter.
Named parameters are also mapped, in example scale key of Affine is mapped on scale parameter of Affine augmenter.

One interesting augementation option when doing background removal task is replacing backgrounds with random
images. We support this with BackgroundReplacer augmenter:

  1. augmentation:
  2. BackgroundReplacer:
  3. path: ./bg #path to folder with backgrounds
  4. rate: 0.5 #fraction of original backgrounds to preserve

Freezing and Unfreezing encoder

Freezing encoder is often used with transfer learning. If you want to start with frozen encoder just add

  1. freeze_encoder: true
  2. stages:
  3. - epochs: 10 #Let's go for 10 epochs with frozen encoder
  4. - epochs: 100 #Now let's go for 100 epochs with trainable encoder
  5. unfreeze_encoder: true

in your experiments configuration, then on some stage configuration just add

  1. unfreeze_encoder: true

to stage settings.

Both freeze_encoder and unfreeze_encoder
can be put into the root section and inside the stage.

Note: This option is not supported for DeeplabV3 architecture.

Custom datasets

You can declare your own dataset class as in this example:

  1. from musket_core.datasets import PredictionItem
  2. import os
  3. import imageio
  4. import pandas as pd
  5. import numpy as np
  6. import cv2
  7. class ClassificationDS:
  8. def __init__(self,imgPath):
  9. self.species = ['Black-grass', 'Charlock', 'Cleavers', 'Common Chickweed', 'Common wheat',
  10. 'Fat Hen', 'Loose Silky-bent', 'Maize', 'Scentless Mayweed',
  11. 'Shepherds Purse', 'Small-flowered Cranesbill', 'Sugar beet']
  12. self.data = []
  13. self.targets = []
  14. self.ids = []
  15. for s_id, s in enumerate(self.species):
  16. s_folder = os.path.join(imgPath,s)
  17. for file in os.listdir(s_folder):
  18. self.data.append(os.path.join(s_folder, file))
  19. self.targets.append(s_id)
  20. self.ids.append(file)
  21. def __len__(self):
  22. return len(self.data)
  23. def __getitem__(self, item):
  24. item_file = self.data[item]
  25. target = self.targets[item]
  26. t = np.zeros(len(self.species))
  27. t[target] = 1.0
  28. image = self.read_image(item_file, (224,224))
  29. return PredictionItem(self.ids[item], image, t)
  30. def read_image(self, filepath, target_size=None):
  31. img = cv2.imread(filepath, cv2.IMREAD_COLOR)
  32. img = cv2.resize(img.copy(), target_size, interpolation = cv2.INTER_AREA)
  33. return img
  34. def getTrain()->datasets.DataSet:
  35. return ClassificationDS("images/")

Now, if this python code sits somewhere in python files located in modules folder of the project, and that file is referred by imports instruction, following YAML can refer it:

  1. dataset:
  2. getTrain: []

dataset sets the main training dataset.

datasets sets up a list of available data sets to be referred by other entities.

Multi output classification

Sometimes you need to create network that performs several classification tasks at the same moment, in this situation
you need to declare classes , activation and loss as the lists of class counts, activation functions and losses
like in the following snippet:

  1. classes: [ 4, 4 ] #define the number of classes
  2. activation: [sigmoid,sigmoid]
  3. loss:
  4. - binary_crossentropy
  5. - binary_crossentropy
  6. primary_metric: val_loss #the most interesting metric is val_binary_accuracy
  7. primary_metric_mode: min

it is also very likely that you need to change primary metric to val_loss

preparing dataset for multi output classification:

When you use multi output classification your prediction item y should be a list of numpy arrays, like in
the following sample:

  1. return PredictionItem(self.ids[item], image, [t0,t1,t2])
  2. `

length of this list should be equal to the number of network outputs

Multistage training

Sometimes you need to split your training into several stages. You can easily do it by adding several stage entries
in your experiment configuration file.

stages instruction allows to set up stages of the train process, where for each stage it is possible to set some specific training options like the number of epochs, learning rate, loss, callbacks, etc.
Full list of stage properties can be found here.

  1. stages:
  2. - epochs: 100 #Let's go for 100 epochs
  3. - epochs: 100 #Let's go for 100 epochs
  4. - epochs: 100 #Let's go for 100 epochs
  1. stages:
  2. - epochs: 6 #Train for 6 epochs
  3. negatives: none #do not include negative examples in your training set
  4. validation_negatives: real #validation should contain all negative examples
  5. - lr: 0.0001 #let's use different starting learning rate
  6. epochs: 6
  7. negatives: real
  8. validation_negatives: real
  9. - loss: lovasz_loss #let's override loss function
  10. lr: 0.00001
  11. epochs: 6
  12. initial_weights: ./fpn-resnext2/weights/best-0.1.weights #let's load weights from this file

Balancing your data

One common case is the situation when part of your images does not contain any objects of interest, like in
Airbus ship detection challenge. More over your data may
be to heavily inbalanced, so you may want to rebalance it. Alternatively you may want to inject some additional
images that do not contain objects of interest to decrease amount of false positives that will be produced by the framework.

These scenarios are supported by negatives and
validation_negatives settings of training stage configuration,
these settings accept following values:

  • none - exclude negative examples from the data
  • real - include all negative examples
  • integer number(1 or 2 or anything), how many negative examples should be included per one positive example
  1. stages:
  2. - epochs: 6 #Train for 6 epochs
  3. negatives: none #do not include negative examples in your training set
  4. validation_negatives: real #validation should contain all negative examples
  5. - lr: 0.0001 #let's use different starting learning rate
  6. epochs: 6
  7. negatives: real
  8. validation_negatives: real
  9. - loss: lovasz_loss #let's override loss function
  10. lr: 0.00001
  11. epochs: 6
  12. initial_weights: ./fpn-resnext2/weights/best-0.1.weights #let's load weights from this file

if you are using this setting your dataset class must support isPositive method which returns true for indexes
which contain positive examples:

  1. def isPositive(self, item):
  2. pixels=self.ddd.get_group(self.ids[item])["EncodedPixels"]
  3. for mask in pixels:
  4. if isinstance(mask, str):
  5. return True;
  6. return False

Advanced learning rates

Dynamic learning rates

Example

As told in Cyclical learning rates for training neural networks CLR policies can provide quicker converge for some neural network tasks and architectures.

Example2

We support them by adopting Brad Kenstler CLR callback for Keras.

If you want to use them, just add CyclicLR in your experiment configuration file as shown below:

  1. callbacks:
  2. EarlyStopping:
  3. patience: 40
  4. monitor: val_binary_accuracy
  5. verbose: 1
  6. CyclicLR:
  7. base_lr: 0.0001
  8. max_lr: 0.01
  9. mode: triangular2
  10. step_size: 300

There are also ReduceLROnPlateau and LRVariator options to modify learning rate on the fly.

LR Finder

Estimating optimal learning rate for your model is an important thing, we support this by using slightly changed
version of Pavel Surmenok - Keras LR Finder

  1. cfg = classification.parse(config.yaml)
  2. ds = SimplePNGMaskDataSet("./train","./train_mask") - ???????????????????
  3. finder=cfg.lr_find(ds,start_lr=0.00001,end_lr=1,epochs=5)
  4. finder.plot_loss(n_skip_beginning=20, n_skip_end=5)
  5. plt.show()
  6. finder.plot_loss_change(sma=20, n_skip_beginning=20, n_skip_end=5, y_lim=(-0.01, 0.01))
  7. plt.show()

will result in this couple of helpful images:

image

image

Training on crops

Your images can be too large to train model on them. In this case you probably want to train model on crops. All
that you need to do is to specify number of splits per axis. For example, following lines in config

  1. shape: [768, 768, 3]
  2. crops: 3

will lead to splitting each image/mask into 9 cells (3 horizontal splits and 3 vertical splits) and training model on these splits.
Augmentations will be run separately on each cell.
crops property sets the number of single dimension cells.

During prediction time, your images will be split into these cells, prediction will be executed on each cell, and then results
will be assembled in single final mask. Thus the whole process of cropping will be invisible from a consumer perspective.

Using trained model

Okey, our model is trained, now we need to actually do image classification. Let’s say, we need to run image classification on
images in the directory and store results in csv file:

  1. predictions = []
  2. images = []
  3. #Now let's use best model from fold 0 to do image segmentation on images from images_to_segment
  4. preds = cfg.predict_all_to_array(dataset_test, 0, 0)
  5. for i, item in enumerate(dataset_test):
  6. images.append(dataset_test.get_id(i))
  7. p = np.argmax(preds[i])
  8. predictions.append(dataset_test.get_label(p))
  9. #Let's store results in csv
  10. df = pd.DataFrame.from_dict({'file': images, 'species': predictions})
  11. df.to_csv('submission.csv', index=False)

Ensembling predictions

And what if you want to ensemble models from several folds? Just pass a list of fold numbers to
predict_all_to_array like in the following example:

  1. cfg.predict_all_to_array(dataset_test, [0,1,2,3,4], 0)

Another supported option is to ensemble results from extra test time augmentation (flips) by adding keyword arg ttflips=True.

Custom evaluation code

Sometimes you need to run custom evaluation code. In such case you may use: evaluateAll method, which provides an iterator
on the batches containing original images, training masks and predicted masks

  1. for batch in cfg.evaluateAll(ds,2):
  2. for i in range(len(batch.predicted_maps_aug)):
  3. masks = ds.get_masks(batch.data[i])
  4. for d in range(1,20):
  5. cur_seg = binary_opening(batch.predicted_maps_aug[i].arr > d/20, np.expand_dims(disk(2), -1))
  6. cm = rle.masks_as_images(rle.multi_rle_encode(cur_seg))
  7. pr = f2(masks, cm);
  8. total[d]=total[d]+pr

Accessing model

You may get trained keras model by calling: cfg.load_model(fold, stage).

Analyzing experiments results

Okey, we have done a lot of experiments and now we need to compare the results and understand what works better. This repository
contains script which may be used to analyze folder containing sub folders
with experiment configurations and results. This script gathers all configurations, diffs them by doing structural diff, then
for each configuration it averages metrics for all folds and generates csv file containing metrics and parameters that
was actually changed in your experiment like in the following example

This script accepts following arguments:

  • inputFolder - root folder to search for experiments configurations and results
  • output - file to store aggregated metrics
  • onlyMetric - if you specify this option all other metrics will not be written in the report file
  • sortBy - metric that should be used to sort results

Example:

  1. python analize.py --inputFolder ./experiments --output ./result.py

What is supported?

At this moment classification pipeline supports following pre-trained models:

Each architecture also supports some specific options, list of options is documented in segmentation RAML library.

Supported augmentations are documented in augmentation RAML library.

Callbacks are documented in callbacks RAML library.

Custom architectures, callbacks, metrics

Classification pipeline uses keras custom objects registry to find entities, so if you need to use
custom loss function, activation or metric all that you need to do is to register it in Keras as:

  1. keras.utils.get_custom_objects()["my_loss"]= my_loss

If you want to inject new architecture, you should register it in classification.custom_models dictionary.

For example:

  1. classification.custom.models['MyUnet']=MyUnet

where MyUnet is a function that accepts architecture parameters as arguments and returns an instance
of keras model.