Model Tuning


In [1]:
import matchzoo as mz
train_raw = mz.datasets.toy.load_data('train')
dev_raw = mz.datasets.toy.load_data('dev')
test_raw = mz.datasets.toy.load_data('test')


Using TensorFlow backend.

basic Usage

A couple things are needed by the tuner:

  • a model with a parameters filled
  • preprocessed training data
  • preprocessed testing data

Since MatchZoo models have pre-defined hyper-spaces, the tuner can start tuning right away once you have the data ready.

prepare the data


In [2]:
preprocessor = mz.models.DenseBaseline.get_default_preprocessor()
train = preprocessor.fit_transform(train_raw, verbose=0)
dev = preprocessor.transform(dev_raw, verbose=0)
test = preprocessor.transform(test_raw, verbose=0)

prepare the model


In [3]:
model = mz.models.DenseBaseline()
model.params['input_shapes'] = preprocessor.context['input_shapes']
model.params['task'] = mz.tasks.Ranking()

start tuning


In [4]:
tuner = mz.auto.Tuner(
    params=model.params,
    train_data=train,
    test_data=dev,
    num_runs=5
)
results = tuner.tune()


Run #1
Score: 0.08333333333333333
model_class                   <class 'matchzoo.models.dense_baseline.DenseBaseline'>
input_shapes                  [(30,), (30,)]
task                          Ranking Task
optimizer                     adam
with_multi_layer_perceptron   True
mlp_num_units                 404
mlp_num_layers                4
mlp_num_fan_out               88
mlp_activation_func           relu

Run #2
Score: 0.125
model_class                   <class 'matchzoo.models.dense_baseline.DenseBaseline'>
input_shapes                  [(30,), (30,)]
task                          Ranking Task
optimizer                     adam
with_multi_layer_perceptron   True
mlp_num_units                 17
mlp_num_layers                3
mlp_num_fan_out               96
mlp_activation_func           relu

Run #3
Score: 0.125
model_class                   <class 'matchzoo.models.dense_baseline.DenseBaseline'>
input_shapes                  [(30,), (30,)]
task                          Ranking Task
optimizer                     adam
with_multi_layer_perceptron   True
mlp_num_units                 293
mlp_num_layers                2
mlp_num_fan_out               120
mlp_activation_func           relu

Run #4
Score: 0.5
model_class                   <class 'matchzoo.models.dense_baseline.DenseBaseline'>
input_shapes                  [(30,), (30,)]
task                          Ranking Task
optimizer                     adam
with_multi_layer_perceptron   True
mlp_num_units                 458
mlp_num_layers                2
mlp_num_fan_out               56
mlp_activation_func           relu

Run #5
Score: 0.16666666666666666
model_class                   <class 'matchzoo.models.dense_baseline.DenseBaseline'>
input_shapes                  [(30,), (30,)]
task                          Ranking Task
optimizer                     adam
with_multi_layer_perceptron   True
mlp_num_units                 313
mlp_num_layers                2
mlp_num_fan_out               36
mlp_activation_func           relu

view the best hyper-parameter set


In [5]:
results['best']


Out[5]:
{'#': 4,
 'params': <matchzoo.engine.param_table.ParamTable at 0x13dfcfef0>,
 'sample': {'mlp_num_fan_out': 56.0,
  'mlp_num_layers': 2.0,
  'mlp_num_units': 458.0},
 'score': 0.5}

In [6]:
results['best']['params'].to_frame()


Out[6]:
Name Description Value Hyper-Space
0 model_class Model class. Used internally for save/load. Ch... <class 'matchzoo.models.dense_baseline.DenseBa... None
1 input_shapes Dependent on the model and data. Should be set... [(30,), (30,)] None
2 task Decides model output shape, loss, and metrics. Ranking Task None
3 optimizer None adam None
4 with_multi_layer_perceptron A flag of whether a multiple layer perceptron ... True None
5 mlp_num_units Number of units in first `mlp_num_layers` layers. 458 quantitative uniform distribution in [16, 512...
6 mlp_num_layers Number of layers of the multiple layer percetron. 2 quantitative uniform distribution in [1, 5), ...
7 mlp_num_fan_out Number of units of the layer that connects the... 56 quantitative uniform distribution in [4, 128)...
8 mlp_activation_func Activation function used in the multiple layer... relu None

understading hyper-space

model.params.hyper_space reprensents the model's hyper-parameters search space, which is the cross-product of individual hyper parameter's hyper space. When a Tuner builds a model, for each hyper parameter in model.params, if the hyper-parameter has a hyper-space, then a sample will be taken in the space. However, if the hyper-parameter does not have a hyper-space, then the default value of the hyper-parameter will be used.


In [7]:
model.params.hyper_space


Out[7]:
{'mlp_num_units': <hyperopt.pyll.base.Apply at 0x13c5967f0>,
 'mlp_num_layers': <hyperopt.pyll.base.Apply at 0x13f7982b0>,
 'mlp_num_fan_out': <hyperopt.pyll.base.Apply at 0x13c5a2b70>}

In a DenseBaseline model, only mlp_num_units, mlp_num_layers, and mlp_num_fan_out have pre-defined hyper-space. In other words, only these hyper-parameters will change values during a tuning. Other hyper-parameters, like mlp_activation_func, are fixed and will not change.


In [8]:
def sample_and_build(params):
    sample = mz.hyper_spaces.sample(params.hyper_space)
    print('if sampled:', sample, '\n')
    params.update(sample)
    print('the built model will have:\n')
    print(params, '\n\n\n')

for _ in range(3):
    sample_and_build(model.params)


if sampled: {'mlp_num_fan_out': 60.0, 'mlp_num_layers': 4.0, 'mlp_num_units': 146.0} 

the built model will have:

model_class                   <class 'matchzoo.models.dense_baseline.DenseBaseline'>
input_shapes                  [(30,), (30,)]
task                          Ranking Task
optimizer                     adam
with_multi_layer_perceptron   True
mlp_num_units                 146
mlp_num_layers                4
mlp_num_fan_out               60
mlp_activation_func           relu 



if sampled: {'mlp_num_fan_out': 64.0, 'mlp_num_layers': 1.0, 'mlp_num_units': 436.0} 

the built model will have:

model_class                   <class 'matchzoo.models.dense_baseline.DenseBaseline'>
input_shapes                  [(30,), (30,)]
task                          Ranking Task
optimizer                     adam
with_multi_layer_perceptron   True
mlp_num_units                 436
mlp_num_layers                1
mlp_num_fan_out               64
mlp_activation_func           relu 



if sampled: {'mlp_num_fan_out': 44.0, 'mlp_num_layers': 4.0, 'mlp_num_units': 48.0} 

the built model will have:

model_class                   <class 'matchzoo.models.dense_baseline.DenseBaseline'>
input_shapes                  [(30,), (30,)]
task                          Ranking Task
optimizer                     adam
with_multi_layer_perceptron   True
mlp_num_units                 48
mlp_num_layers                4
mlp_num_fan_out               44
mlp_activation_func           relu 



This is similar to the process of a tuner sampling model hyper-parameters, but with one key difference: a tuner's hyper-space is suggestive. This means the sampling process in a tuner is not truely random but skewed. Scores of the past samples affect future choices: a tuner with more runs knows better about its hyper-space, and take samples in a way that will likely yields better scores.

For more details, consult tuner's backend: hyperopt, and the search algorithm tuner uses: Tree of Parzen Estimators (TPE)

Hyper-spaces can also be represented in a human-readable format.


In [9]:
print(model.params.get('mlp_num_units').hyper_space)


quantitative uniform distribution in  [16, 512), with a step size of 1

In [10]:
model.params.to_frame()[['Name', 'Hyper-Space']]


Out[10]:
Name Hyper-Space
0 model_class None
1 input_shapes None
2 task None
3 optimizer None
4 with_multi_layer_perceptron None
5 mlp_num_units quantitative uniform distribution in [16, 512...
6 mlp_num_layers quantitative uniform distribution in [1, 5), ...
7 mlp_num_fan_out quantitative uniform distribution in [4, 128)...
8 mlp_activation_func None

setting hyper-space

What if I want the tuner to choose optimizer among adam, adagrad, and rmsprop?


In [11]:
model.params.get('optimizer').hyper_space = mz.hyper_spaces.choice(['adam', 'adagrad', 'rmsprop'])

In [12]:
for _ in range(10):
    print(mz.hyper_spaces.sample(model.params.hyper_space))


{'mlp_num_fan_out': 24.0, 'mlp_num_layers': 4.0, 'mlp_num_units': 439.0, 'optimizer': 'adagrad'}
{'mlp_num_fan_out': 112.0, 'mlp_num_layers': 3.0, 'mlp_num_units': 299.0, 'optimizer': 'rmsprop'}
{'mlp_num_fan_out': 76.0, 'mlp_num_layers': 3.0, 'mlp_num_units': 353.0, 'optimizer': 'adagrad'}
{'mlp_num_fan_out': 120.0, 'mlp_num_layers': 3.0, 'mlp_num_units': 496.0, 'optimizer': 'adagrad'}
{'mlp_num_fan_out': 64.0, 'mlp_num_layers': 2.0, 'mlp_num_units': 290.0, 'optimizer': 'adagrad'}
{'mlp_num_fan_out': 72.0, 'mlp_num_layers': 5.0, 'mlp_num_units': 360.0, 'optimizer': 'adagrad'}
{'mlp_num_fan_out': 68.0, 'mlp_num_layers': 1.0, 'mlp_num_units': 100.0, 'optimizer': 'adagrad'}
{'mlp_num_fan_out': 20.0, 'mlp_num_layers': 2.0, 'mlp_num_units': 511.0, 'optimizer': 'adagrad'}
{'mlp_num_fan_out': 92.0, 'mlp_num_layers': 2.0, 'mlp_num_units': 18.0, 'optimizer': 'adagrad'}
{'mlp_num_fan_out': 128.0, 'mlp_num_layers': 5.0, 'mlp_num_units': 297.0, 'optimizer': 'adagrad'}

What about setting mlp_num_layers to a fixed value of 2?


In [13]:
model.params['mlp_num_layers'] = 2
model.params.get('mlp_num_layers').hyper_space = None

In [14]:
for _ in range(10):
    print(mz.hyper_spaces.sample(model.params.hyper_space))


{'mlp_num_fan_out': 100.0, 'mlp_num_units': 168.0, 'optimizer': 'adam'}
{'mlp_num_fan_out': 68.0, 'mlp_num_units': 487.0, 'optimizer': 'adam'}
{'mlp_num_fan_out': 124.0, 'mlp_num_units': 356.0, 'optimizer': 'adagrad'}
{'mlp_num_fan_out': 116.0, 'mlp_num_units': 499.0, 'optimizer': 'rmsprop'}
{'mlp_num_fan_out': 32.0, 'mlp_num_units': 120.0, 'optimizer': 'rmsprop'}
{'mlp_num_fan_out': 84.0, 'mlp_num_units': 442.0, 'optimizer': 'adagrad'}
{'mlp_num_fan_out': 92.0, 'mlp_num_units': 142.0, 'optimizer': 'adagrad'}
{'mlp_num_fan_out': 68.0, 'mlp_num_units': 472.0, 'optimizer': 'adagrad'}
{'mlp_num_fan_out': 32.0, 'mlp_num_units': 343.0, 'optimizer': 'adagrad'}
{'mlp_num_fan_out': 68.0, 'mlp_num_units': 19.0, 'optimizer': 'adam'}

using callbacks

To save the model during the tuning process, use mz.auto.tuner.callbacks.SaveModel.


In [15]:
tuner.num_runs = 2
tuner.callbacks.append(mz.auto.tuner.callbacks.SaveModel())
results = tuner.tune()


WARNING: `tune` does not affect the tuner's inner state, so
                each new call to `tune` starts fresh. In other words,
                hyperspaces are suggestive only within the same `tune` call.
Run #1
Score: 0.07142857142857142
model_class                   <class 'matchzoo.models.dense_baseline.DenseBaseline'>
input_shapes                  [(30,), (30,)]
task                          Ranking Task
optimizer                     rmsprop
with_multi_layer_perceptron   True
mlp_num_units                 360
mlp_num_layers                2
mlp_num_fan_out               92
mlp_activation_func           relu

Run #2
Score: 0.08333333333333333
model_class                   <class 'matchzoo.models.dense_baseline.DenseBaseline'>
input_shapes                  [(30,), (30,)]
task                          Ranking Task
optimizer                     adagrad
with_multi_layer_perceptron   True
mlp_num_units                 99
mlp_num_layers                2
mlp_num_fan_out               84
mlp_activation_func           relu

This will save all built models to your mz.USER_TUNED_MODELS_DIR, and can be loaded by:


In [16]:
best_model_id = results['best']['model_id']
mz.load_model(mz.USER_TUNED_MODELS_DIR.joinpath(best_model_id))


Out[16]:
<matchzoo.models.dense_baseline.DenseBaseline at 0x13deda6a0>

To load a pre-trained embedding layer into a built model during a tuning process, use mz.auto.tuner.callbacks.LoadEmbeddingMatrix.


In [17]:
toy_embedding = mz.datasets.toy.load_embedding()
preprocessor = mz.models.DUET.get_default_preprocessor()
train = preprocessor.fit_transform(train_raw, verbose=0)
dev = preprocessor.transform(dev_raw, verbose=0)
params = mz.models.DUET.get_default_params()
params['task'] = mz.tasks.Ranking()
params.update(preprocessor.context)
params['embedding_output_dim'] = toy_embedding.output_dim

In [18]:
embedding_matrix = toy_embedding.build_matrix(preprocessor.context['vocab_unit'].state['term_index'])
load_embedding_matrix_callback = mz.auto.tuner.callbacks.LoadEmbeddingMatrix(embedding_matrix)

In [19]:
tuner = mz.auto.tuner.Tuner(
    params=params,
    train_data=train,
    test_data=dev,
    num_runs=1
)
tuner.callbacks.append(load_embedding_matrix_callback)
results = tuner.tune()


Run #1
Score: 0.125
model_class                   <class 'matchzoo.models.duet.DUET'>
input_shapes                  [(30,), (30,)]
task                          Ranking Task
optimizer                     adam
with_embedding                True
embedding_input_dim           285
embedding_output_dim          2
embedding_trainable           True
lm_filters                    32
lm_hidden_sizes               [32]
dm_filters                    32
dm_kernel_size                3
dm_q_hidden_size              32
dm_d_mpool                    3
dm_hidden_sizes               [32]
padding                       same
activation_func               relu
dropout_rate                  0.14

make your own callbacks

To build your own callbacks, inherit mz.auto.tuner.callbacks.Callback and overrides corresponding methods.

A run proceeds in the following way:

  • run start (callback)
  • build model
  • build end (callback)
  • fit and evaluate model
  • collect result
  • run end (callback)

This process is repeated for num_runs times in a tuner.

For example, say I want to verify if my embedding matrix is correctly loaded.


In [20]:
import numpy as np

class ValidateEmbedding(mz.auto.tuner.callbacks.Callback):
    def __init__(self, embedding_matrix):
        self._matrix = embedding_matrix
        
    def on_build_end(self, tuner, model):
        loaded_matrix = model.get_embedding_layer().get_weights()[0]
        if np.isclose(self._matrix, loaded_matrix).all():
            print("Yes! The my embedding is correctly loaded!")

In [21]:
validate_embedding_matrix_callback = ValidateEmbedding(embedding_matrix)

In [22]:
tuner = mz.auto.tuner.Tuner(
    params=params,
    train_data=train,
    test_data=dev,
    num_runs=1,
    callbacks=[load_embedding_matrix_callback, validate_embedding_matrix_callback]
)
tuner.callbacks.append(load_embedding_matrix_callback)
results = tuner.tune()


Yes! The my embedding is correctly loaded!
Run #1
Score: 0.08333333333333333
model_class                   <class 'matchzoo.models.duet.DUET'>
input_shapes                  [(30,), (30,)]
task                          Ranking Task
optimizer                     adam
with_embedding                True
embedding_input_dim           285
embedding_output_dim          2
embedding_trainable           True
lm_filters                    32
lm_hidden_sizes               [32]
dm_filters                    32
dm_kernel_size                3
dm_q_hidden_size              32
dm_d_mpool                    3
dm_hidden_sizes               [32]
padding                       same
activation_func               relu
dropout_rate                  0.44