Tutorial


In order to use StructED to a specific task one should implement three interfaces which contains four functions:

  1. Loss function from ITaskLoss interface.
  2. An argmax over the linear discriminant function from IInference interface.
  3. An argmax over the loss-augmented linear discriminant function from IInference interface.
  4. A function for computing Phi from IFeatureFunctions interface.

Introduction


In this tutorial we will demonstrate how to use and add new task to the StructED package. We will give code examples for the Task-Loss, Predict - Inference and Feature Functions interfaces. For this tutorial we will use small subset from a dummy data that was generated in our lab in order to debug the vowel duration measurement problem.

Before we begin!

You can use this tutorial in two ways:
  1. Run the code as we implemented it:
  2. In order to run the tutorials you can simply run the bash script in any of the tutorials directories in our GitHub repository. The tutorials can be found under the tutorials-code directory under dummy folder.

  3. Write the code yourself:
  4. We should setup our development environment first. Open a Java project and add the StructED jar file to your build path (it can be found under the bin directory at StructED repository). You can download all StructED source codes and jar files from GitHub.
Now let's begin!

Dummy Data


The dummy data we use in this tutorial was generated in order to debug the vowel duration measurement problem. Hence, the label for each example is composed from two numbers, the start time and end time ($Y\in{\mathbb{R}}^2$), and the input data is an arbitrary length vector of numbers for 0,1 domain, ($X \in{\{0,1\}}^d$ and $d\in{\mathbb{N}}$).

For example: 8-17 0:0 1:0 2:1 3:0 4:0 5:0 6:0 7:0 8:1 9:1 10:1 11:0 12:1 13:1 14:1 15:1 16:1 17:0 18:0 19:0

Here, the first two numbers are the label which indicates that the signal turns on at the eighth element of the vector and turns off at the seventeenth element of the vector. Each vector data is composed from the index of the feature, we did this to support space features, and the feature data separated by a colon(:).

Our goal is to find a function $ f$ that gets as input the vector data and outputs the start time where the signal turns on and end time where it turns off. Notice, we add a little bit off noise to the vector data for example at the second index or at the eleventh index, we want to fine $ f$ that isn't sensitive to small amount of noise in the data. We know that this is a toy example, but it demonstrate really good the use of the package and the integration with it. We assume that the signal turns on only once.

The DB can be found under the db/ folder in this tutorial zip file.

The Code


Here, we present what classes do we need to add and what interfaces do we need to implement. We provide the source code for all of the classes and interfaces.

Task Loss

First we define out measure of performance. Every task has defines its own function. In out package, in order to create new loss function one should implement the ITaskLoss interface. In our problem settings we use the following loss function:

\begin{equation} \label{eq:loss} \ell ((t_{s}, t_{e}),(\hat{t_{s}}, \hat{t_{e}})) = max{\{|(\hat{t_{s}}-\hat{t_{e}}) - (t_{s} - t_{e})| - \epsilon ,0\}} \end{equation}

In words, the loss will be the max between zero to the difference between the predicted signals length to its actual length and we minus epsilon. This means that we allow the classifier to be mistaken by at most epsilon.

For this we create new Java class inside that implements the ITaskLoss interface. The code for the implementation of this class is attached to this tutorial.

Inference

Now we turn to define our inference functions. Every inference class should implement the IInference interface.
In our problem settings the inference will be a brute force. We assume that at the beginning and at the end there is a gap of three frames, which means the beginning of the of the signal can not be in the first 3 frames and the end of the signal can not be at the last three frames, this can be redefined otherwise if needed.
Out inference will go over all the possible time pairs for the start and end times, starting from minimum gap to maximum gap.

In general we need the Inference class to implement the following:

\begin{equation} \label{eq:decoding} \hat{y}_{w}(x) = argmax_{\mathcal{y} \in \mathcal{Y}} ~ w^\top \phi(x, y) \end{equation}

and,

\begin{equation} \label{eq:augmented-loss} \hat{y}_{w}(x) = argmax_{\mathcal{y} \in \mathcal{Y}} ~ w^\top \phi(x, y) + \ell (y,\hat{y}) \end{equation}

For this we create new Java class inside the com.structed.models.inference package, this class should implement the IInference interface. The code for the implementation of this class is attached to this tutorial or can be found at the tutorial folder.

Feature Functions

We now add the feature functions. Every feature function class should implement the IFeatureFunctions interface. Since we want to recognize the pick of the start of the signal and the decrease of the end signal we implemented the following feature functions:

  1. Difference between the element at the start index to the element at index start - 1
  2. Difference between the element at the start index to the element at index start - 2
  3. Difference between the element at the end index to the element at index end + 1
  4. Difference between the element at the end index to the element at index end + 2
  5. Difference between the mean of the signal from start to end to the mean of the signal from start to start - 3
  6. Difference between the mean of the signal from start to end to the mean of the signal from end to end + 3
We expect that the value of the above feature functions will be high when we reach the true start time and end time, and low otherwise.

For this we create new Java class inside the com.structed.data.featureFunctions package, this class should implement the IFeatureFunctions interface. The code for the implementation of this class is attached to this tutorial or can be found at the tutorial folder.

Storing the train/test examples

StructED uses an InstanceContainer class to store its examples, the instance container contains the data/labels and paths for all the examples. Each example is stored in a data structure called Example1D. We implemented a standard reader for this cases, when using StructED to different tasks one can either use one of our standard readers or to implement a new one.

Running The Code


Now we can create a StructEDModel object with the interfaces we have just implemented, all we have left to do is to choose the model. Here is a snippet of such code (the complete code can be found at the package repository under tutorials-code folder):

                
        Logger.info("Dummy data example.");
        int readerType = 0;
        int epochNum = 3;
        int isAvg = 1;
        int numExamples2Display = 3;
        // <the path to the train dummy data>
        String trainPath = "data/train.txt"; 
        // <the path to the test dummy data>
        String testPath = "data/test.txt"; 

        // load the data
        Reader reader = getReader(readerType);
        InstancesContainer dummyTrainInstances = reader.readData(trainPath, Consts.SPACE, 
              Consts.COLON_SPLITTER);
        InstancesContainer dummyTestInstances = reader.readData(testPath, 
              Consts.SPACE, Consts.COLON_SPLITTER);
        if ( dummyTrainInstances.getSize() == 0 ) return;

        // ======= PASSIVE AGGRESSIVE ====== //
        // init the first weight vector
        Vector W = new Vector() {{put(0, 0.0);}}; 
        // model parameters
        ArrayList<Double> arguments = new ArrayList<Double>(){{add(3.0);}}; 
        // task loss parameters
        ArrayList<Double> task_loss_params = new ArrayList<Double>(){{add(1.0);}}; 

        // create the model
        StructEDModel dummy_model = new StructEDModel(W, new PassiveAggressive(), new TaskLossDummyData(),
                new InferenceDummyData(), null, new FeatureFunctionsDummy(), arguments); 
        // train
        dummy_model.train(dummyTrainInstances, task_loss_params, null, epochNum, isAvg); 
        // predict
        ArrayList<PredictedLabels> labels = dummy_model.predict(dummyTestInstances, task_loss_params, 
                numExamples2Display); 

        // print the prediction
        for(int i=0 ; i<dummyTestInstances.getSize() ; i++)
            Logger.info("Y = "+dummyTestInstances.getInstance(i).getLabel()+", Y_HAT = "+
                labels.get(i).firstKey());
        Logger.info("");       

                    


You can see that changing the model is pretty straightforward:

                
        // ======= CRF ====== //
        // init the first weight vector
        W = new Vector() {{put(0, 0.0);}}; 
        // model parameters
        arguments = new ArrayList<Double>(){{add(0.1);add(0.1);}}; 

        // create the model
        StructEDModel dummy_model_crf = new StructEDModel(W, new CRF(), new TaskLossDummyData(),
                new InferenceDummyData(), null, new FeatureFunctionsDummy(), arguments); 
        // train
        dummy_model_crf.train(dummyTrainInstances, task_loss_params, null, epochNum, isAvg); 
        // predict
        labels = dummy_model_crf.predict(dummyTestInstances, task_loss_params, numExamples2Display); 

        // print the prediction
        for(int i=0 ; i<dummyTestInstances.getSize() ; i++)
            Logger.info("Y = "+dummyTestInstances.getInstance(i).getLabel()+", Y_HAT = "+
                  labels.get(i).firstKey());
        Logger.info("");