diff --git a/src/mlpack/methods/perceptron/perceptron_main.cpp b/src/mlpack/methods/perceptron/perceptron_main.cpp index 49fec086c3..f22d077478 100644 --- a/src/mlpack/methods/perceptron/perceptron_main.cpp +++ b/src/mlpack/methods/perceptron/perceptron_main.cpp @@ -17,108 +17,238 @@ using namespace std; using namespace arma; PROGRAM_INFO("Perceptron", - "This program implements a perceptron, which is a single level " - "Neural Network. The perceptron makes its predictions based on " - "a linear predictor function combining a set of weights with the feature " - "vector.\n" - "The perceptron learning rule is able to converge, given enough iterations " - "using the --iterations (-i) parameter, if the data supplied is " - "linearly separable. " - "\n" - "The Perceptron is parameterized by a matrix of weight vectors which " - "denotes the numerical weights of the Neural Network." - "\n" - "This program allows training of a perceptron, and then application of " - "the learned perceptron to a test dataset. To train a perceptron, " - "a training dataset must be passed to --train_file (-t). Labels can either " - "be present as the last dimension of the training dataset, or given " - "explicitly with the --labels_file (-l) parameter." - "\n" - "A test file is given through the --test_file (-T) parameter. The " - "predicted labels for the test set will be stored in the file specified by " - "the --output_file (-o) parameter." + "This program implements a perceptron, which is a single level neural " + "network. The perceptron makes its predictions based on a linear predictor " + "function combining a set of weights with the feature vector. The " + "perceptron learning rule is able to converge, given enough iterations " + "using the --max_iterations (-i) parameter, if the data supplied is " + "linearly separable. The perceptron is parameterized by a matrix of weight" + " vectors that denote the numerical weights of the neural network." + "\n\n" + "This program allows loading a perceptron from a model (-i) or training a " + "perceptron given training data (-t), or both those things at once. In " + "addition, this program allows classification on a test dataset (-T) and " + "will save the classification results to the given output file (-o). The " + "perceptron model itself may be saved with a file specified using the -m " + "option." + "\n\n" + "The training data given with the -t option should have class labels as its" + " last dimension (so, if the training data is in CSV format, labels should " + "be the last column). Alternately, the -l (--labels_file) option may be " + "used to specify a separate file of labels." + "\n\n" + "All these options make it easy to train a perceptron, and then re-use that" + " perceptron for later classification. The invocation below trains a " + "perceptron on 'training_data.csv' (and 'training_labels.csv)' and saves " + "the model to 'perceptron.xml'." + "\n\n" + "$ perceptron -t training_data.csv -l training_labels.csv -m perceptron.csv" + "\n\n" + "Then, this model can be re-used for classification on 'test_data.csv'. " + "The example below does precisely that, saving the predicted classes to " + "'predictions.csv'." + "\n\n" + "$ perceptron -i perceptron.xml -T test_data.csv -o predictions.csv" + "\n\n" + "Note that all of the options may be specified at once: predictions may be " + "calculated right after training a model, and model training can occur even" + " if an existing perceptron model is passed with -i (--input_model). " + "However, note that the number of classes and the dimensionality of all " + "data must match. So you cannot pass a perceptron model trained on 2 " + "classes and then re-train with a 4-class dataset. Similarly, attempting " + "classification on a 3-dimensional dataset with a perceptron that has been " + "trained on 8 dimensions will cause an error." ); -// Necessary parameters -PARAM_STRING_REQ("train_file", "A file containing the training set.", "t"); +// Training parameters. +PARAM_STRING("training_file", "A file containing the training set.", "t", ""); PARAM_STRING("labels_file", "A file containing labels for the training set.", - "l",""); -PARAM_STRING_REQ("test_file", "A file containing the test set.", "T"); + "l", ""); +PARAM_INT("max_iterations","The maximum number of iterations the perceptron is " + "to be run", "M", 1000); -// Optional parameters. -PARAM_STRING("output", "The file in which the predicted labels for the test set" - " will be written.", "o", "output.csv"); -PARAM_INT("iterations","The maximum number of iterations the perceptron is " - "to be run", "i", 1000); +// Model loading/saving. +PARAM_STRING("input_model", "File containing input perceptron model.", "i", ""); +PARAM_STRING("output_model", "File to save trained perceptron model to.", "m", + ""); + +// Testing/classification parameters. +PARAM_STRING("test_file", "A file containing the test set.", "T", ""); +PARAM_STRING("output_file", "The file in which the predicted labels for the " + "test set will be written.", "o", "output.csv"); + +// When we save a model, we must also save the class mappings. So we use this +// auxiliary structure to store both the perceptron and the mapping, and we'll +// save this. +class PerceptronModel +{ + private: + Perceptron<>& p; + arma::vec& map; + + public: + PerceptronModel(Perceptron<>& p, arma::vec& map) : p(p), map(map) { } + + template + void Serialize(Archive& ar, const unsigned int /* version */) + { + ar & data::CreateNVP(p, "perceptron"); + ar & data::CreateNVP(map, "mappings"); + } +}; int main(int argc, char** argv) { CLI::ParseCommandLine(argc, argv); - // Get reference dataset filename. - const string trainingDataFilename = CLI::GetParam("train_file"); - mat trainingData; - data::Load(trainingDataFilename, trainingData, true); + // First, get all parameters and validate them. + const string trainingDataFile = CLI::GetParam("training_file"); + const string labelsFile = CLI::GetParam("labels_file"); + const string inputModelFile = CLI::GetParam("input_model"); + const string testDataFile = CLI::GetParam("test_file"); + const string outputModelFile = CLI::GetParam("output_model"); + const string outputFile = CLI::GetParam("output_file"); + const size_t maxIterations = (size_t) CLI::GetParam("max_iterations"); - const string labelsFilename = CLI::GetParam("labels_file"); - // Load labels. - mat labelsIn; + // We must either load a model or train a model. + if (inputModelFile == "" && trainingDataFile == "") + Log::Fatal << "Either an input model must be specified with --input_model " + << "or training data must be given (--training_file)!" << endl; - // Did the user pass in labels? - if (CLI::HasParam("labels_file")) + // If the user isn't going to save the output model or any predictions, we + // should issue a warning. + if (outputModelFile == "" && testDataFile == "") + Log::Warn << "Output will not be saved! (Neither --test_file nor " + << "--output_model are specified.)" << endl; + + // Now, load our model, if there is one. + Perceptron<>* p = NULL; + arma::vec mappings; + if (inputModelFile != "") { + Log::Info << "Loading saved perceptron from model file '" << inputModelFile + << "'." << endl; + + // The parameters here are invalid, but we are about to load the model + // anyway... + p = new Perceptron<>(0, 0); + PerceptronModel pm(*p, mappings); // Also load class mappings. + data::Load(inputModelFile, "perceptron_model", pm, true); + } + + // Next, load the training data and labels (if they have been given). + if (trainingDataFile != "") + { + Log::Info << "Training perceptron on dataset '" << trainingDataFile; + if (labelsFile != "") + Log::Info << "' with labels in '" << labelsFile << "'"; + else + Log::Info << "'"; + Log::Info << " for a maximum of " << maxIterations << " iterations." + << endl; + + mat trainingData; + data::Load(trainingDataFile, trainingData, true); + // Load labels. - const string labelsFilename = CLI::GetParam("labels_file"); - data::Load(labelsFilename, labelsIn, true); + mat labelsIn; + + // Did the user pass in labels? + if (CLI::HasParam("labels_file")) + { + // Load labels. + const string labelsFile = CLI::GetParam("labels_file"); + data::Load(labelsFile, labelsIn, true); + } + else + { + // Use the last row of the training data as the labels. + Log::Info << "Using the last dimension of training set as labels." + << endl; + labelsIn = trainingData.row(trainingData.n_rows - 1).t(); + trainingData.shed_row(trainingData.n_rows - 1); + } + + // Do the labels need to be transposed? + if (labelsIn.n_rows == 1) + labelsIn = labelsIn.t(); + + // Normalize the labels. + Col labels; + data::NormalizeLabels(labelsIn.unsafe_col(0), labels, mappings); + + // Now, if we haven't already created a perceptron, do it. Otherwise, make + // sure the dimensions are right, then continue training. + if (p == NULL) + { + // Create and train the classifier. + Timer::Start("training"); + p = new Perceptron<>(trainingData, labels.t(), max(labels) + 1, + maxIterations); + Timer::Stop("training"); + } + else + { + // Check dimensionality. + if (p->Weights().n_rows != trainingData.n_rows) + { + Log::Fatal << "Perceptron from '" << inputModelFile << "' is built on " + << "data with " << p->Weights().n_rows << " dimensions, but data in" + << " '" << trainingDataFile << "' has " << trainingData.n_rows + << "dimensions!" << endl; + } + + // Check the number of labels. + if (max(labels) + 1 > p->Weights().n_cols) + { + Log::Fatal << "Perceptron from '" << inputModelFile << "' has " + << p->Weights().n_cols << " classes, but the training data has " + << max(labels) + 1 << " classes!" << endl; + } + + // Now train. + Timer::Start("training"); + p->MaxIterations() = maxIterations; + p->Train(trainingData, labels.t()); + Timer::Stop("training"); + } } - else + + // Now, the training procedure is complete. Do we have any test data? + if (testDataFile != "") { - // Use the last row of the training data as the labels. - Log::Info << "Using the last dimension of training set as labels." << endl; - labelsIn = trainingData.row(trainingData.n_rows - 1).t(); - trainingData.shed_row(trainingData.n_rows - 1); + Log::Info << "Classifying dataset '" << testDataFile << "'." << endl; + mat testData; + data::Load(testDataFile, testData, true); + + if (testData.n_rows != p->Weights().n_rows) + { + Log::Fatal << "Test data dimensionality (" << testData.n_rows << ") must " + << "be the same as the dimensionality of the perceptron (" + << p->Weights().n_rows << ")!" << endl; + } + + // Time the running of the perceptron classifier. + Row predictedLabels(testData.n_cols); + Timer::Start("testing"); + p->Classify(testData, predictedLabels); + Timer::Stop("testing"); + + // Un-normalize labels to prepare output. + vec results; + data::RevertLabels(predictedLabels.t(), mappings, results); + + // Save the predictedLabels, but we have to transpose them. + data::Save(outputFile, results, false /* non-fatal */, false); } - // Do the labels need to be transposed? - if (labelsIn.n_rows == 1) + // Lastly, do we need to save the output model? + if (outputModelFile != "") { - labelsIn = labelsIn.t(); + PerceptronModel pm(*p, mappings); + data::Save(outputModelFile, "perceptron_model", pm); } - // Normalize the labels. - Col labels; - vec mappings; - data::NormalizeLabels(labelsIn.unsafe_col(0), labels, mappings); - - // Load test dataset. - const string testingDataFilename = CLI::GetParam("test_file"); - mat testingData; - data::Load(testingDataFilename, testingData, true); - if (testingData.n_rows != trainingData.n_rows) - { - Log::Fatal << "Test data dimensionality (" << testingData.n_rows << ") " - << "must be the same as training data (" << trainingData.n_rows - 1 - << ")!" << std::endl; - } - - int iterations = CLI::GetParam("iterations"); - - // Create and train the classifier. - Timer::Start("Training"); - Perceptron<> p(trainingData, labels.t(), max(labels) + 1, iterations); - Timer::Stop("Training"); - - // Time the running of the Perceptron Classifier. - Row predictedLabels(testingData.n_cols); - Timer::Start("Testing"); - p.Classify(testingData, predictedLabels); - Timer::Stop("Testing"); - - // Un-normalize labels to prepare output. - vec results; - data::RevertLabels(predictedLabels.t(), mappings, results); - - // saving the predictedLabels in the transposed manner in output - const string outputFilename = CLI::GetParam("output"); - data::Save(outputFilename, results, true, false); + // Clean up memory. + delete p; }