dlib C++ Library - graph_labeling_ex.cpp

    // The contents of this file are in the public domain. See LICENSE_FOR_EXAMPLE_PROGRAMS.txt
    /*

        This is an example illustrating the use of the graph_labeler and
        structural_graph_labeling_trainer objects.

        Suppose you have a bunch of objects and you need to label each of them as true or
        false.  Suppose further that knowing the labels of some of these objects tells you
        something about the likely label of the others.  This is common in a number of domains.
        For example, in image segmentation problems you need to label each pixel, and knowing
        the labels of neighboring pixels gives you information about the likely label since
        neighboring pixels will often have the same label.

        We can generalize this problem by saying that we have a graph and our task is to label
        each node in the graph as true or false.  Additionally, the edges in the graph connect
        nodes which are likely to share the same label.  In this example program, each node
        will have a feature vector which contains information which helps tell if the node
        should be labeled as true or false.  The edges also contain feature vectors which give
        information indicating how strong the edge's labeling consistency constraint should be.
        This is useful since some nodes will have uninformative feature vectors and the only
        way to tell how they should be labeled is by looking at their neighbor's labels.

        Therefore, this program will show you how to learn two things using machine learning.
        The first is a linear classifier which operates on each node and predicts if it should
        be labeled as true or false.  The second thing is a linear function of the edge
        vectors.  This function outputs a penalty for giving two nodes connected by an edge
        differing labels.  The graph_labeler object puts these two things together and uses
        them to compute a labeling which takes both into account.  In what follows, we will use
        a structural SVM method to find the parameters of these linear functions which minimize
        the number of mistakes made by a graph_labeler.

        Finally, you might also consider reading the book Structured Prediction and Learning in
        Computer Vision by Sebastian Nowozin and Christoph H. Lampert since it contains a good
        introduction to machine learning methods such as the algorithm implemented by the
        structural_graph_labeling_trainer.
    */

    #include <dlib/svm_threaded.h>
    #include <iostream>

    using namespace std;
    using namespace dlib;

    // ----------------------------------------------------------------------------------------

    // The first thing we do is define the kind of graph object we will be using.
    // Here we are saying there will be 2-D vectors at each node and 1-D vectors at
    // each edge.  (You should read the matrix_ex.cpp example program for an introduction
    // to the matrix object.)
    typedef matrix<double,2,1> node_vector_type;
    typedef matrix<double,1,1> edge_vector_type;
    typedef graph<node_vector_type, edge_vector_type>::kernel_1a_c graph_type;

    // ----------------------------------------------------------------------------------------

    template <
        typename graph_type,
        typename labels_type
        >
    void make_training_examples(
        dlib::array<graph_type>& samples,
        labels_type& labels
    )
    {
        /*
            This function makes 3 graphs we will use for training.   All of them
            will contain 4 nodes and have the structure shown below:

              (0)-----(1)
               |       |
               |       |
               |       |
              (3)-----(2)

            In this example, each node has a 2-D vector.  The first element of this vector
            is 1 when the node should have a label of false while the second element has
            a value of 1 when the node should have a label of true.  Additionally, the
            edge vectors will contain a value of 1 when the nodes connected by the edge
            should share the same label and a value of 0 otherwise.

            We want to see that the machine learning method is able to figure out how
            these features relate to the labels.  If it is successful it will create a
            graph_labeler which can predict the correct labels for these and other
            similarly constructed graphs.

            Finally, note that these tools require all values in the edge vectors to be >= 0.
            However, the node vectors may contain both positive and negative values.
        */

        samples.clear();
        labels.clear();

        std::vector<bool> label;
        graph_type g;

        // ---------------------------
        g.set_number_of_nodes(4);
        label.resize(g.number_of_nodes());
        // store the vector [0,1] into node 0.  Also label it as true.
        g.node(0).data = 0, 1; label[0] = true;
        // store the vector [0,0] into node 1.
        g.node(1).data = 0, 0; label[1] = true;  // Note that this node's vector doesn't tell us how to label it.
                                                 // We need to take the edges into account to get it right.
        // store the vector [1,0] into node 2.
        g.node(2).data = 1, 0; label[2] = false;
        // store the vector [0,0] into node 3.
        g.node(3).data = 0, 0; label[3] = false;

        // Add the 4 edges as shown in the ASCII art above.
        g.add_edge(0,1);
        g.add_edge(1,2);
        g.add_edge(2,3);
        g.add_edge(3,0);

        // set the 1-D vector for the edge between node 0 and 1 to the value of 1.
        edge(g,0,1) = 1;
        // set the 1-D vector for the edge between node 1 and 2 to the value of 0.
        edge(g,1,2) = 0;
        edge(g,2,3) = 1;
        edge(g,3,0) = 0;
        // output the graph and its label.
        samples.push_back(g);
        labels.push_back(label);

        // ---------------------------
        g.set_number_of_nodes(4);
        label.resize(g.number_of_nodes());
        g.node(0).data = 0, 1; label[0] = true;
        g.node(1).data = 0, 1; label[1] = true;
        g.node(2).data = 1, 0; label[2] = false;
        g.node(3).data = 1, 0; label[3] = false;

        g.add_edge(0,1);
        g.add_edge(1,2);
        g.add_edge(2,3);
        g.add_edge(3,0);

        // This time, we have strong edges between all the nodes.  The machine learning
        // tools will have to learn that when the node information conflicts with the
        // edge constraints that the node information should dominate.
        edge(g,0,1) = 1;
        edge(g,1,2) = 1;
        edge(g,2,3) = 1;
        edge(g,3,0) = 1;
        samples.push_back(g);
        labels.push_back(label);
        // ---------------------------

        g.set_number_of_nodes(4);
        label.resize(g.number_of_nodes());
        g.node(0).data = 1, 0; label[0] = false;
        g.node(1).data = 1, 0; label[1] = false;
        g.node(2).data = 1, 0; label[2] = false;
        g.node(3).data = 0, 0; label[3] = false;

        g.add_edge(0,1);
        g.add_edge(1,2);
        g.add_edge(2,3);
        g.add_edge(3,0);

        edge(g,0,1) = 0;
        edge(g,1,2) = 0;
        edge(g,2,3) = 1;
        edge(g,3,0) = 0;
        samples.push_back(g);
        labels.push_back(label);
        // ---------------------------

    }

    // ----------------------------------------------------------------------------------------

    int main()
    {
        try
        {
            // Get the training samples we defined above.
            dlib::array<graph_type> samples;
            std::vector<std::vector<bool> > labels;
            make_training_examples(samples, labels);

            // Create a structural SVM trainer for graph labeling problems.  The vector_type
            // needs to be set to a type capable of holding node or edge vectors.
            typedef matrix<double,0,1> vector_type;
            structural_graph_labeling_trainer<vector_type> trainer;
            // This is the usual SVM C parameter.  Larger values make the trainer try
            // harder to fit the training data but might result in overfitting.  You
            // should set this value to whatever gives the best cross-validation results.
            trainer.set_c(10);

            // Do 3-fold cross-validation and print the results.  In this case it will
            // indicate that all nodes were correctly classified.
            cout << "3-fold cross-validation: " << cross_validate_graph_labeling_trainer(trainer, samples, labels, 3) << endl;

            // Since the trainer is working well.  Let's have it make a graph_labeler
            // based on the training data.
            graph_labeler<vector_type> labeler = trainer.train(samples, labels);

            /*
                Let's try the graph_labeler on a new test graph.  In particular, let's
                use one with 5 nodes as shown below:

                (0 F)-----(1 T)
                  |         |
                  |         |
                  |         |
                (3 T)-----(2 T)------(4 T)

                I have annotated each node with either T or F to indicate the correct
                output (true or false).
            */
            graph_type g;
            g.set_number_of_nodes(5);
            g.node(0).data = 1, 0;  // Node data indicates a false node.
            g.node(1).data = 0, 1;  // Node data indicates a true node.
            g.node(2).data = 0, 0;  // Node data is ambiguous.
            g.node(3).data = 0, 0;  // Node data is ambiguous.
            g.node(4).data = 0.1, 0; // Node data slightly indicates a false node.

            g.add_edge(0,1);
            g.add_edge(1,2);
            g.add_edge(2,3);
            g.add_edge(3,0);
            g.add_edge(2,4);

            // Set the edges up so nodes 1, 2, 3, and 4 are all strongly connected.
            edge(g,0,1) = 0;
            edge(g,1,2) = 1;
            edge(g,2,3) = 1;
            edge(g,3,0) = 0;
            edge(g,2,4) = 1;

            // The output of this shows all the nodes are correctly labeled.
            cout << "Predicted labels: " << endl;
            std::vector<bool> temp = labeler(g);
            for (unsigned long i = 0; i < temp.size(); ++i)
                cout << " " << i << ": " << temp[i] << endl;

            // Breaking the strong labeling consistency link between node 1 and 2 causes
            // nodes 2, 3, and 4 to flip to false.  This is because of their connection
            // to node 4 which has a small preference for false.
            edge(g,1,2) = 0;
            cout << "Predicted labels: " << endl;
            temp = labeler(g);
            for (unsigned long i = 0; i < temp.size(); ++i)
                cout << " " << i << ": " << temp[i] << endl;
        }
        catch (std::exception& e)
        {
            cout << "Error, an exception was thrown!" << endl;
            cout << e.what() << endl;
        }
    }