Configuration Tutorial

This tutorial will show how to use Tribuo's configuration and provenance systems to build models on MNIST (because we wouldn't be doing ML without an MNIST demo). We'll focus on logistic regression, show how many different trainers can be stored in the same configuration, and how the provenance system allows the configuration for a specific run to be regenerated. We'll also briefly look at Tribuo's feature transformation system and see how that integrates into configuration and provenance.

Setup

You'll need to get a copy of the MNIST dataset in libSVM format.

wget https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass/mnist.bz2

wget https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass/mnist.t.bz2

Then unzip them using your preferred method (e.g. bunzip2 <file-name>).

It's Java, so first we load in the necessary Tribuo jars. Here we're using the classification experiments jar, along with the json interop jar to read and write the provenance information.

In [1]:
%jars ./olcut-core-5.1.4-SNAPSHOT.jar
%jars ./tribuo-classification-experiments-4.0.0-SNAPSHOT-jar-with-dependencies.jar
%jars ./tribuo-json-4.0.0-SNAPSHOT-jar-with-dependencies.jar

Now lets import the packages we need. We'll use a few file manipulation things from Java, and then Tribuo's core packages, the transformation packages, the classification package, classification evaluation package, and then a few things that relate to the provenance system.

In [2]:
import java.nio.file.Files;
import java.nio.file.Paths;
In [3]:
import org.tribuo.*;
import org.tribuo.util.Util;
import org.tribuo.transform.*;
import org.tribuo.transform.transformations.LinearScalingTransformation;
import org.tribuo.classification.*;
import org.tribuo.classification.evaluation.*;
import com.oracle.labs.mlrg.olcut.config.ConfigurationManager;
import com.oracle.labs.mlrg.olcut.provenance.*;
import com.oracle.labs.mlrg.olcut.provenance.primitives.*;
import com.oracle.labs.mlrg.olcut.config.json.JsonConfigFactory;

By default OLCUT's ConfigurationManager only understands XML files, this snippet adds JSON support to all ConfigurationManagers in the running JVM. It can be added dynamically on the command line by supplying --config-file-format <fully-qualified-class-name> where the class name is for example com.oracle.labs.mlrg.olcut.config.json.JsonConfigFactory, if you're using OLCUT's CLI options processing.

In [4]:
ConfigurationManager.addFileFormatFactory(new JsonConfigFactory())

Using a configuration file

We're going to read in an example configuration file, in JSON format. This configuration knows about a bunch of different trainers, and also the training and testing MNIST data sources. In the tutorials directory we supply both the JSON and XML versions of this file, and the remainder of this tutorial is completely agnostic to which one is used.

In [5]:
String configFile = "example-config.json";
String.join("\n",Files.readAllLines(Paths.get(configFile)))
Out[5]:
{
  "config" : {
    "components" : [ {
      "name" : "mnist-test",
      "type" : "org.tribuo.datasource.LibSVMDataSource",
      "export" : "false",
      "import" : "false",
      "properties" : {
        "path" : "mnist.t",
        "outputFactory" : "label-factory"
      }
    }, {
      "name" : "mnist-train",
      "type" : "org.tribuo.datasource.LibSVMDataSource",
      "export" : "false",
      "import" : "false",
      "properties" : {
        "path" : "mnist",
        "outputFactory" : "label-factory"
      }
    }, {
      "name" : "adagrad",
      "type" : "org.tribuo.math.optimisers.AdaGrad",
      "export" : "false",
      "import" : "false",
      "properties" : {
        "epsilon" : "0.01",
        "initialLearningRate" : "0.5"
      }
    }, {
      "name" : "log",
      "type" : "org.tribuo.classification.sgd.objectives.LogMulticlass",
      "export" : "false",
      "import" : "false"
    }, {
      "name" : "label-factory",
      "type" : "org.tribuo.classification.LabelFactory",
      "export" : "false",
      "import" : "false"
    }, {
      "name" : "gini",
      "type" : "org.tribuo.classification.dtree.impurity.GiniIndex",
      "export" : "false",
      "import" : "false"
    }, {
      "name" : "cart",
      "type" : "org.tribuo.classification.dtree.CARTClassificationTrainer",
      "export" : "false",
      "import" : "false",
      "properties" : {
        "maxDepth" : "6",
        "impurity" : "gini",
        "seed" : "12345",
        "fractionFeaturesInSplit" : "0.5"
      }
    }, {
      "name" : "entropy",
      "type" : "org.tribuo.classification.dtree.impurity.Entropy",
      "export" : "false",
      "import" : "false"
    }, {
      "name" : "logistic",
      "type" : "org.tribuo.classification.sgd.linear.LinearSGDTrainer",
      "export" : "false",
      "import" : "false",
      "properties" : {
        "seed" : "1",
        "minibatchSize" : "1",
        "epochs" : "2",
        "optimiser" : "adagrad",
        "objective" : "log",
        "loggingInterval" : "10000"
      }
    }, {
      "name" : "xgboost",
      "type" : "org.tribuo.classification.xgboost.XGBoostClassificationTrainer",
      "export" : "false",
      "import" : "false",
      "properties" : {
        "numTrees" : "10",
        "maxDepth" : "4",
        "eta" : "0.5",
        "seed" : "1",
        "minChildWeight" : "1.0",
        "subsample" : "1.0",
        "nThread" : "6",
        "gamma" : "0.1"
      }
    } ]
  }
}

Now we'll make a ConfigurationManager and hand it the configuration file to load. Our configuration system also supports CLI options which can load things out of the supplied configuration files. We have examples of this in each of the simple TrainTest demo classes in each prediction backend.

In [6]:
var cm = new ConfigurationManager(configFile);

First we'll load in the training and testing DataSources (as instances of LibSVMDataSource), pass them into two Datasets to aggregate the appropriate metadata, and we'll make the evaluator for later use.

In [7]:
DataSource<Label> mnistTrain = (DataSource<Label>) cm.lookup("mnist-train");
DataSource<Label> mnistTest = (DataSource<Label>) cm.lookup("mnist-test");
var trainData = new MutableDataset<>(mnistTrain);
var testData = new MutableDataset<>(mnistTest);
var evaluator = new LabelEvaluator();
System.out.println(String.format("Training data size = %d, number of features = %d, number of classes = %d",trainData.size(),trainData.getFeatureMap().size(),trainData.getOutputInfo().size()));
System.out.println(String.format("Testing data size = %d, number of features = %d, number of classes = %d",testData.size(),testData.getFeatureMap().size(),testData.getOutputInfo().size()));
Training data size = 60000, number of features = 717, number of classes = 10
Testing data size = 10000, number of features = 668, number of classes = 10

Loading in trainers from the configuration

Our configuration file contains a number of different trainers, so let's pull them out and take a look.

The first one we'll see is a CART decision tree, with a max tree depth of 6.

In [8]:
var cart = (Trainer<Label>) cm.lookup("cart");
cart
Out[8]:
CARTClassificationTrainer(maxDepth=6,minChildWeight=5.0,fractionFeaturesInSplit=0.5,impurity=GiniIndex,seed=12345)

Next we'll load an XGBoost trainer, using 10 trees, 6 computation threads, and some regularisation parameters.

In [9]:
var xgb = (Trainer<Label>) cm.lookup("xgboost");
xgb
Out[9]:
XGBoostTrainer(numTrees=10,parameters{colsample_bytree=1.0, silent=1, seed=1, max_depth=4, booster=gbtree, objective=multi:softprob, lambda=1.0, eta=0.5, nthread=6, alpha=1.0, subsample=1.0, gamma=0.1, min_child_weight=1.0})

Finally we'll load in a logistic regression trainer, using AdaGrad as the gradient optimizer.

In [10]:
var logistic = (Trainer<Label>) cm.lookup("logistic");
logistic
Out[10]:
LinearSGDTrainer(objective=LogMulticlass,optimiser=AdaGrad(initialLearningRate=0.5,epsilon=0.01,initialValue=0.0),epochs=2,minibatchSize=1,seed=1)

We can also load a list in containing all the Trainer implementations in this config file. Note: the config system by default returns the same instance when it's queried for the same named config. So the list contains references to the objects we've already loaded.

In [11]:
var trainers = (List<Trainer>) cm.lookupAll(Trainer.class);
System.out.println("Loaded " + trainers.size() + " trainers.");
Loaded 3 trainers.

Training the model and extracting configuration

We're going to focus on the logistic regression trainer now, so let's train a logistic regression model on our MNIST training set.

In [12]:
var lrStartTime = System.currentTimeMillis();
var lrModel = logistic.train(trainData);
var lrEndTime = System.currentTimeMillis();
System.out.println("Training logistic regression took " + Util.formatDuration(lrStartTime,lrEndTime));
Training logistic regression took (00:00:04:934)

We can inspect the trained model for it's provenance, as we saw in the Classification tutorial.

The new step is extracting a configuration from that provenance. The ProvenanceUtil.extractConfiguration() call returns a List<ConfigurationData> which is the object representation of a configuration file. We can see that it's extracted configurations for 5 objects from our single model, we'll look at those after we've written out the file.

In [13]:
var provenance = lrModel.getProvenance();
var provConfig = ProvenanceUtil.extractConfiguration(provenance);
provConfig.size()
Out[13]:
5

The ConfigurationManager is the way we can generate a configuration file from the object representation. We create a new ConfigurationManager, add the configuration we extracted from the provenance, and then write it out to a new JSON file.

In [14]:
var outputFile = "mnist-logistic-config.json";
var newCM = new ConfigurationManager();
newCM.addConfiguration(provConfig);
newCM.save(new File(outputFile),true);
String.join("\n",Files.readAllLines(Paths.get(outputFile)))
Out[14]:
{
  "config" : {
    "components" : [ {
      "name" : "libsvmdatasource-1",
      "type" : "org.tribuo.datasource.LibSVMDataSource",
      "export" : "false",
      "import" : "false",
      "properties" : {
        "path" : "/Users/apocock/Development/Tribuo/tutorials/mnist",
        "maxFeatureID" : "780",
        "zeroIndexed" : "false",
        "outputFactory" : "labelfactory-4",
        "url" : "file:/Users/apocock/Development/Tribuo/tutorials/mnist"
      }
    }, {
      "name" : "linearsgdtrainer-0",
      "type" : "org.tribuo.classification.sgd.linear.LinearSGDTrainer",
      "export" : "false",
      "import" : "false",
      "properties" : {
        "seed" : "1",
        "minibatchSize" : "1",
        "shuffle" : "true",
        "epochs" : "2",
        "optimiser" : "adagrad-2",
        "objective" : "logmulticlass-3",
        "loggingInterval" : "10000"
      }
    }, {
      "name" : "adagrad-2",
      "type" : "org.tribuo.math.optimisers.AdaGrad",
      "export" : "false",
      "import" : "false",
      "properties" : {
        "epsilon" : "0.01",
        "initialLearningRate" : "0.5",
        "initialValue" : "0.0"
      }
    }, {
      "name" : "labelfactory-4",
      "type" : "org.tribuo.classification.LabelFactory",
      "export" : "false",
      "import" : "false"
    }, {
      "name" : "logmulticlass-3",
      "type" : "org.tribuo.classification.sgd.objectives.LogMulticlass",
      "export" : "false",
      "import" : "false"
    } ]
  }
}

The five elements of the configuration are: the training data "libsvmdatasource-1", the logistic regression "linearsgdtrainer-0", the training log loss function "logmulticlass-3", the AdaGrad gradient optimizer "adagrad-2", and the label factory "labelfactory-4". The only unexpected part is the LabelFactory which is the factory that converts Strings into Label instances.

Rebuilding a model from it's configuration

Now to reconstruct our model, we can load in the Trainer and DataSource from the new ConfigurationManager, pass the source into a Dataset, and finally call train on the new trainer supplying the new dataset.

In [15]:
var newTrainer = (Trainer<Label>) newCM.lookup("linearsgdtrainer-0");
var newSource = (DataSource<Label>) newCM.lookup("libsvmdatasource-1");
var newDataset = new MutableDataset<>(newSource);
var newModel = newTrainer.train(newDataset, Collections.singletonMap("reconfigured-model",new BooleanProvenance("reconfigured-model",true)));

First we'll confirm that the old model and new models aren't equal (as they have different timestamps, among other provenance checks).

In [16]:
lrModel.equals(newModel)
Out[16]:
false

Now we'll evaluate the first model:

In [17]:
var lrEvaluator = evaluator.evaluate(lrModel,testData);
System.out.println(lrEvaluator.toString());
System.out.println(lrEvaluator.getConfusionMatrix().toString());
Class                           n          tp          fn          fp      recall        prec          f1
0                             980         904          76          21       0.922       0.977       0.922
1                           1,135       1,072          63          18       0.944       0.983       0.944
2                           1,032         856         176          56       0.829       0.939       0.829
3                           1,010         844         166          84       0.836       0.909       0.836
4                             982         888          94          72       0.904       0.925       0.904
5                             892         751         141         143       0.842       0.840       0.842
6                             958         938          20         139       0.979       0.871       0.979
7                           1,028         963          65         133       0.937       0.879       0.937
8                             974         892          82         363       0.916       0.711       0.916
9                           1,009         801         208          62       0.794       0.928       0.794
Total                      10,000       8,909       1,091       1,091
Accuracy                                                                    0.891
Micro Average                                                               0.891       0.891       0.891
Macro Average                                                               0.890       0.896       0.890
Balanced Error Rate                                                         0.110
               0       1       2       3       4       5       6       7       8       9
0            904       0       2       3       1      20      26       4      18       2
1              0   1,072       7       3       0       2       6       2      43       0
2              3       6     856      26       5       7      39       8      80       2
3              1       0      13     844       2      64       7      14      62       3
4              0       0       7       2     888       1      22      15      20      27
5              9       1       1      27       6     751      18       7      68       4
6              3       1       2       1       1       9     938       1       2       0
7              1       5      18       6       4       1       0     963       9      21
8              1       3       6       9       9      25      20       6     892       3
9              3       2       0       7      44      14       1      76      61     801

It's about what we'd expect for a linear model on MNIST. Not SOTA, but it'll do for now.

Now let's check the new model:

In [18]:
var newEvaluator = evaluator.evaluate(newModel,testData);
System.out.println(newEvaluator.toString());
System.out.println(newEvaluator.getConfusionMatrix().toString());
Class                           n          tp          fn          fp      recall        prec          f1
0                             980         904          76          21       0.922       0.977       0.922
1                           1,135       1,072          63          18       0.944       0.983       0.944
2                           1,032         856         176          56       0.829       0.939       0.829
3                           1,010         844         166          84       0.836       0.909       0.836
4                             982         888          94          72       0.904       0.925       0.904
5                             892         751         141         143       0.842       0.840       0.842
6                             958         938          20         139       0.979       0.871       0.979
7                           1,028         963          65         133       0.937       0.879       0.937
8                             974         892          82         363       0.916       0.711       0.916
9                           1,009         801         208          62       0.794       0.928       0.794
Total                      10,000       8,909       1,091       1,091
Accuracy                                                                    0.891
Micro Average                                                               0.891       0.891       0.891
Macro Average                                                               0.890       0.896       0.890
Balanced Error Rate                                                         0.110
               0       1       2       3       4       5       6       7       8       9
0            904       0       2       3       1      20      26       4      18       2
1              0   1,072       7       3       0       2       6       2      43       0
2              3       6     856      26       5       7      39       8      80       2
3              1       0      13     844       2      64       7      14      62       3
4              0       0       7       2     888       1      22      15      20      27
5              9       1       1      27       6     751      18       7      68       4
6              3       1       2       1       1       9     938       1       2       0
7              1       5      18       6       4       1       0     963       9      21
8              1       3       6       9       9      25      20       6     892       3
9              3       2       0       7      44      14       1      76      61     801

We can see that both models perform identically. This is because our provenance system records the RNG seeds used at all points, and Tribuo is scrupulous about how and when it uses PRNGs. If you find a model reconstruction that gives a different answer (unless you're using XGBoost, which has some non-determinism beyond our control) then file an issue on our GitHub as that's a bug.

What else lives in the Provenance?

These evaluations have provenance in the same way the models do, and we can use a pretty printer in OLCUT to make it a little more human readable.

In addition to the configuration information like the gradient optimiser and RNG seed, the provenance includes run specific information like the "reconfigured-model" flag we added, along with a hash of the data, timestamps for the various data files involved, and a timestamp for the model creation and dataset creation.

In [19]:
var evalProvenance = newEvaluator.getProvenance();
System.out.println(ProvenanceUtil.formattedProvenanceString(evalProvenance));
EvaluationProvenance(
	class-name = org.tribuo.provenance.EvaluationProvenance
	model-provenance = LinearSGDModel(
			class-name = org.tribuo.classification.sgd.linear.LinearSGDModel
			dataset = MutableDataset(
					class-name = org.tribuo.MutableDataset
					datasource = LibSVMDataSource(
							class-name = org.tribuo.datasource.LibSVMDataSource
							path = /Users/apocock/Development/Tribuo/tutorials/mnist
							maxFeatureID = 780
							zeroIndexed = false
							outputFactory = LabelFactory(
									class-name = org.tribuo.classification.LabelFactory
								)
							url = file:/Users/apocock/Development/Tribuo/tutorials/mnist
							resource-hash = C42B75FCB54DDA11D97AF9BD6E3950B41C93B4F44EEB9608AA115F79A5979E88
							file-modified-time = 2020-07-06T10:52:01.776-04:00
							datasource-creation-time = 2020-07-21T13:28:11.674954-04:00
							host-short-name = DataSource
						)
					transformations = List[]
					is-sequence = false
					is-dense = false
					num-examples = 60000
					num-features = 717
					num-outputs = 10
					tribuo-version = 4.0.0-SNAPSHOT
				)
			trainer = LinearSGDTrainer(
					class-name = org.tribuo.classification.sgd.linear.LinearSGDTrainer
					seed = 1
					minibatchSize = 1
					shuffle = true
					epochs = 2
					optimiser = AdaGrad(
							class-name = org.tribuo.math.optimisers.AdaGrad
							epsilon = 0.01
							initialLearningRate = 0.5
							initialValue = 0.0
							host-short-name = StochasticGradientOptimiser
						)
					objective = LogMulticlass(
							class-name = org.tribuo.classification.sgd.objectives.LogMulticlass
							host-short-name = LabelObjective
						)
					loggingInterval = 10000
					train-invocation-count = 0
					is-sequence = false
					host-short-name = Trainer
				)
			trained-at = 2020-07-21T13:28:17.640344-04:00
			instance-values = Map{
			reconfigured-model=true
			}
			tribuo-version = 4.0.0-SNAPSHOT
		)
	dataset-provenance = MutableDataset(
			class-name = org.tribuo.MutableDataset
			datasource = LibSVMDataSource(
					class-name = org.tribuo.datasource.LibSVMDataSource
					path = /Users/apocock/Development/Tribuo/tutorials/mnist.t
					maxFeatureID = 778
					zeroIndexed = false
					outputFactory = LabelFactory(
							class-name = org.tribuo.classification.LabelFactory
						)
					url = file:/Users/apocock/Development/Tribuo/tutorials/mnist.t
					resource-hash = 31FFC3CFB1824D0994D57172BB8CA68882E578859E1EE1BD9FC1F2F15BFD06A2
					file-modified-time = 2020-07-06T10:52:01.888-04:00
					datasource-creation-time = 2020-07-21T13:28:01.871792-04:00
					host-short-name = DataSource
				)
			transformations = List[]
			is-sequence = false
			is-dense = false
			num-examples = 10000
			num-features = 668
			num-outputs = 10
			tribuo-version = 4.0.0-SNAPSHOT
		)
	tribuo-version = 4.0.0-SNAPSHOT
)

Feature Transformations

We can take the new trainer, wrap it programmatically in a TransfomTrainer which rescales the input features into the range [0,2], and still generate provenance and configuration automatically as the model is trained.

In [20]:
var transformations = new TransformationMap(List.of(new LinearScalingTransformation(0,1)));
var transformed = new TransformTrainer(newTrainer,transformations);
var transformStart = System.currentTimeMillis();
var transformedModel = transformed.train(newDataset);
var transformEnd = System.currentTimeMillis();
System.out.println("Training transformed logistic regression took " + Util.formatDuration(transformStart,transformEnd));
Training transformed logistic regression took (00:00:08:542)

Now we'll evaluate the rescaled model. Here we see that rescaling the data into the zero-one range improves the linear model performance a couple of percent as all the data is now on the same scale. As expected it's still not SOTA, but we're not using a huge CNN or some other complex model, for that you can try out our TensorFlow interface, or use the XGBoost trainer we loaded in from the original configuration file.

In [21]:
LabelEvaluation transformedEvaluator = evaluator.evaluate(transformedModel,testData);
System.out.println(transformedEvaluator.toString());
System.out.println(transformedEvaluator.getConfusionMatrix().toString());
Class                           n          tp          fn          fp      recall        prec          f1
0                             980         957          23          40       0.977       0.960       0.977
1                           1,135       1,109          26          36       0.977       0.969       0.977
2                           1,032         940          92          90       0.911       0.913       0.911
3                           1,010         927          83         141       0.918       0.868       0.918
4                             982         914          68          73       0.931       0.926       0.931
5                             892         813          79         183       0.911       0.816       0.911
6                             958         892          66          45       0.931       0.952       0.931
7                           1,028         918         110          54       0.893       0.944       0.893
8                             974         753         221          60       0.773       0.926       0.773
9                           1,009         926          83         129       0.918       0.878       0.918
Total                      10,000       9,149         851         851
Accuracy                                                                    0.915
Micro Average                                                               0.915       0.915       0.915
Macro Average                                                               0.914       0.915       0.913
Balanced Error Rate                                                         0.086
               0       1       2       3       4       5       6       7       8       9
0            957       0       1       2       1      12       4       2       1       0
1              0   1,109      10       3       0       2       3       2       6       0
2              4       9     940      18       9       7      11      11      19       4
3              6       0      25     927       0      26       2       7       9       8
4              1       1       7       4     914       0       9       7       4      35
5              7       1       2      30       8     813       9       3      18       1
6              8       2      14       3       8      27     892       2       2       0
7              1       7      17      19       8       1       0     918       1      56
8              7       9      13      46      11      93       7      10     753      25
9              6       7       1      16      28      15       0      10       0     926

We can emit a configuration which includes both the transformation trainer and the original trainer pulled from the old configuration.

In [22]:
var transformedProvConfig = ProvenanceUtil.extractConfiguration(transformedModel.getProvenance());
var transformedOutputFile = "mnist-transformed-logistic-config.json";
newCM = new ConfigurationManager();
newCM.addConfiguration(transformedProvConfig);
newCM.save(new File(transformedOutputFile),true);
String.join("\n",Files.readAllLines(Paths.get(transformedOutputFile)))
Out[22]:
{
  "config" : {
    "components" : [ {
      "name" : "linearscalingtransformation-4",
      "type" : "org.tribuo.transform.transformations.LinearScalingTransformation",
      "export" : "false",
      "import" : "false",
      "properties" : {
        "targetMax" : "1.0",
        "targetMin" : "0.0"
      }
    }, {
      "name" : "labelfactory-7",
      "type" : "org.tribuo.classification.LabelFactory",
      "export" : "false",
      "import" : "false"
    }, {
      "name" : "adagrad-5",
      "type" : "org.tribuo.math.optimisers.AdaGrad",
      "export" : "false",
      "import" : "false",
      "properties" : {
        "epsilon" : "0.01",
        "initialLearningRate" : "0.5",
        "initialValue" : "0.0"
      }
    }, {
      "name" : "linearsgdtrainer-2",
      "type" : "org.tribuo.classification.sgd.linear.LinearSGDTrainer",
      "export" : "false",
      "import" : "false",
      "properties" : {
        "seed" : "1",
        "minibatchSize" : "1",
        "shuffle" : "true",
        "epochs" : "2",
        "optimiser" : "adagrad-5",
        "objective" : "logmulticlass-6",
        "loggingInterval" : "10000"
      }
    }, {
      "name" : "transformtrainer-0",
      "type" : "org.tribuo.transform.TransformTrainer",
      "export" : "false",
      "import" : "false",
      "properties" : {
        "transformations" : "transformationmap-1",
        "densify" : "false",
        "innerTrainer" : "linearsgdtrainer-2"
      }
    }, {
      "name" : "logmulticlass-6",
      "type" : "org.tribuo.classification.sgd.objectives.LogMulticlass",
      "export" : "false",
      "import" : "false"
    }, {
      "name" : "libsvmdatasource-3",
      "type" : "org.tribuo.datasource.LibSVMDataSource",
      "export" : "false",
      "import" : "false",
      "properties" : {
        "path" : "/Users/apocock/Development/Tribuo/tutorials/mnist",
        "maxFeatureID" : "780",
        "zeroIndexed" : "false",
        "outputFactory" : "labelfactory-7",
        "url" : "file:/Users/apocock/Development/Tribuo/tutorials/mnist"
      }
    }, {
      "name" : "transformationmap-1",
      "type" : "org.tribuo.transform.TransformationMap",
      "export" : "false",
      "import" : "false",
      "properties" : {
        "featureTransformationList" : { },
        "globalTransformations" : [ {
          "item" : "linearscalingtransformation-4"
        } ]
      }
    } ]
  }
}

Aside from the names (which have different tag numbers) we can see that this configuration is identical to the previous one, but with the addition of the transformtrainer-0 and it's dependents.

Conclusion

We've taken a closer look at Tribuo's configuration and provenance systems, showing how to train a model using a configuration file, how to inspect the model's provenance, extract it's configuration, and finally how to combine that extracted configuration with other programmatic elements of the Tribuo library (in this case the feature transformation system). We saw that the provenance combines both the configuration of the trainer and the datasource, along with runtime information extracted from the dataset itself (e.g. timestamps and file hashes).

Tribuo's configuration system is integrated into a CLI options/arguments parsing system, which can be used to override elements from the configuration file. The values from the options are then stored in the ConfigurationManager and appear in the provenance and downstream configuration objects as expected. Tribuo also provides a redaction system for configuration files (e.g. to ensure a password isn't stored in the provenance) and for provenance objects themselves (e.g. to remove the data provenance from a trained model), which aids model deployment to untrusted or less trusted systems.