Files
mlpack/src/mlpack/methods/perceptron/perceptron_main.cpp
T
2018-02-08 23:16:23 +05:30

281 lines
10 KiB
C++

/**
* @file perceptron_main.cpp
* @author Udit Saxena
*
* This program runs the Simple Perceptron Classifier.
*
* Perceptrons are simple single-layer binary classifiers, which solve linearly
* separable problems with a linear decision boundary.
*
* mlpack is free software; you may redistribute it and/or modify it under the
* terms of the 3-clause BSD license. You should have received a copy of the
* 3-clause BSD license along with mlpack. If not, see
* http://www.opensource.org/licenses/BSD-3-Clause for more information.
*/
#include <mlpack/prereqs.hpp>
#include <mlpack/core/util/cli.hpp>
#include <mlpack/core/data/normalize_labels.hpp>
#include <mlpack/core/util/mlpack_main.hpp>
#include "perceptron.hpp"
using namespace mlpack;
using namespace mlpack::perceptron;
using namespace mlpack::util;
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. The "
"perceptron learning rule is able to converge, given enough iterations "
"(specified using the " + PRINT_PARAM_STRING("max_iterations") +
" 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 (via the " +
PRINT_PARAM_STRING("input_model") + " parameter) or training a perceptron "
"given training data (via the " + PRINT_PARAM_STRING("training") +
" parameter), or both those things at once. In addition, this program "
"allows classification on a test dataset (via the " +
PRINT_PARAM_STRING("test") + " parameter) and the classification results "
"on the test set may be saved with the " + PRINT_PARAM_STRING("output") +
"output parameter. The perceptron model may be saved with the " +
PRINT_PARAM_STRING("output_model") + " output parameter."
"\n\n"
"The training data given with the " + PRINT_PARAM_STRING("training") +
" option may have class labels as its last dimension (so, if the training "
"data is in CSV format, labels should be the last column). Alternately, "
"the " + PRINT_PARAM_STRING("labels") + " parameter may be used to specify "
"a separate matrix 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 " + PRINT_DATASET("training_data") + " with labels " +
PRINT_DATASET("training_labels") + ", and saves the model to " +
PRINT_MODEL("perceptron") + "."
"\n\n" +
PRINT_CALL("perceptron", "training", "training_data", "labels",
"training_labels", "output_model", "perceptron") +
"\n\n"
"Then, this model can be re-used for classification on the test data " +
PRINT_DATASET("test_data") + ". The example below does precisely that, "
"saving the predicted classes to " + PRINT_DATASET("predictions") + "."
"\n\n" +
PRINT_CALL("perceptron", "input_model", "perceptron", "test", "test_data",
"output", "predictions") +
"\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 the " +
PRINT_PARAM_STRING("input_model") + " parameter. 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.");
// 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;
Col<size_t> map;
public:
Perceptron<>& P() { return p; }
const Perceptron<>& P() const { return p; }
Col<size_t>& Map() { return map; }
const Col<size_t>& Map() const { return map; }
template<typename Archive>
void serialize(Archive& ar, const unsigned int /* version */)
{
ar & BOOST_SERIALIZATION_NVP(p);
ar & BOOST_SERIALIZATION_NVP(map);
}
};
// Training parameters.
PARAM_MATRIX_IN("training", "A matrix containing the training set.", "t");
PARAM_UROW_IN("labels", "A matrix containing labels for the training set.",
"l");
PARAM_INT_IN("max_iterations", "The maximum number of iterations the "
"perceptron is to be run", "n", 1000);
// Model loading/saving.
PARAM_MODEL_IN(PerceptronModel, "input_model", "Input perceptron model.", "m");
PARAM_MODEL_OUT(PerceptronModel, "output_model", "Output for trained perceptron"
" model.", "M");
// Testing/classification parameters.
PARAM_MATRIX_IN("test", "A matrix containing the test set.", "T");
PARAM_UROW_OUT("output", "The matrix in which the predicted labels for the"
" test set will be written.", "o");
static void mlpackMain()
{
// First, get all parameters and validate them.
const size_t maxIterations = (size_t) CLI::GetParam<int>("max_iterations");
// We must either load a model or train a model.
RequireAtLeastOnePassed({ "input_model", "training" }, true);
// If the user isn't going to save the output model or any predictions, we
// should issue a warning.
RequireAtLeastOnePassed({ "output_model", "output" }, false,
"no output will be saved");
ReportIgnoredParam({{ "test", true }}, "output");
// Check parameter validity.
RequireParamValue<int>("max_iterations", [](int x) { return x >= 0; },
true, "maximum number of iterations must be nonnegative");
// Now, load our model, if there is one.
PerceptronModel* p;
if (CLI::HasParam("input_model"))
{
Log::Info << "Using saved perceptron from "
<< CLI::GetPrintableParam<PerceptronModel*>("input_model") << "."
<< endl;
p = CLI::GetParam<PerceptronModel*>("input_model");
}
else
{
p = new PerceptronModel();
}
// Next, load the training data and labels (if they have been given).
if (CLI::HasParam("training"))
{
Log::Info << "Training perceptron on dataset '"
<< CLI::GetPrintableParam<mat>("training");
if (CLI::HasParam("labels"))
{
Log::Info << "' with labels in '"
<< CLI::GetPrintableParam<Row<size_t>>("labels") << "'";
}
else
{
Log::Info << "'";
}
Log::Info << " for a maximum of " << maxIterations << " iterations."
<< endl;
mat trainingData = std::move(CLI::GetParam<mat>("training"));
// Load labels.
Row<size_t> labelsIn;
// Did the user pass in labels?
if (CLI::HasParam("labels"))
{
labelsIn = std::move(CLI::GetParam<Row<size_t>>("labels"));
// Checking the size of the responses and training data
if (labelsIn.n_cols != trainingData.n_cols)
{
Log::Fatal << "The responses must have the same number of columns "
"as the training set." << endl;
}
}
else
{
// Checking the size of training data if no labels are passed
if (trainingData.n_rows < 2)
{
Log::Fatal << "Can't get responses from training data "
"since it has less than 2 rows." << endl;
}
// Use the last row of the training data as the labels.
Log::Info << "Using the last dimension of training set as labels."
<< endl;
labelsIn = arma::conv_to<Row<size_t>>::from(
trainingData.row(trainingData.n_rows - 1));
trainingData.shed_row(trainingData.n_rows - 1);
}
// Normalize the labels.
Row<size_t> labels;
data::NormalizeLabels(labelsIn, labels, p->Map());
const size_t numClasses = p->Map().n_elem;
// Now, if we haven't already created a perceptron, do it. Otherwise, make
// sure the dimensions are right, then continue training.
if (!CLI::HasParam("input_model"))
{
// Create and train the classifier.
Timer::Start("training");
p->P() = Perceptron<>(trainingData, labels, numClasses, maxIterations);
Timer::Stop("training");
}
else
{
// Check dimensionality.
if (p->P().Weights().n_rows != trainingData.n_rows)
{
Log::Fatal << "Perceptron from '"
<< CLI::GetPrintableParam<PerceptronModel*>("input_model")
<< "' is built on data with " << p->P().Weights().n_rows
<< " dimensions, but data in '"
<< CLI::GetPrintableParam<arma::mat>("training") << "' has "
<< trainingData.n_rows << "dimensions!" << endl;
}
// Check the number of labels.
if (numClasses > p->P().Weights().n_cols)
{
Log::Fatal << "Perceptron from '"
<< CLI::GetPrintableParam<PerceptronModel*>("input_model") << "' "
<< "has " << p->P().Weights().n_cols << " classes, but the training"
<< " data has " << numClasses + 1 << " classes!" << endl;
}
// Now train.
Timer::Start("training");
p->P().MaxIterations() = maxIterations;
p->P().Train(trainingData, labels.t(), numClasses);
Timer::Stop("training");
}
}
// Now, the training procedure is complete. Do we have any test data?
if (CLI::HasParam("test"))
{
Log::Info << "Classifying dataset '"
<< CLI::GetPrintableParam<arma::mat>("test") << "'." << endl;
mat testData = std::move(CLI::GetParam<arma::mat>("test"));
if (testData.n_rows != p->P().Weights().n_rows)
{
Log::Fatal << "Test data dimensionality (" << testData.n_rows << ") must "
<< "be the same as the dimensionality of the perceptron ("
<< p->P().Weights().n_rows << ")!" << endl;
}
// Time the running of the perceptron classifier.
Row<size_t> predictedLabels(testData.n_cols);
Timer::Start("testing");
p->P().Classify(testData, predictedLabels);
Timer::Stop("testing");
// Un-normalize labels to prepare output.
Row<size_t> results;
data::RevertLabels(predictedLabels, p->Map(), results);
// Save the predicted labels.
if (CLI::HasParam("output"))
CLI::GetParam<arma::Row<size_t>>("output") = std::move(results);
}
// Lastly, save the output model.
CLI::GetParam<PerceptronModel*>("output_model") = p;
}