/**
 * @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",
    // Short description.
    "An implementation of a perceptron---a single level neural network--=for "
    "classification.  Given labeled data, a perceptron can be trained and saved"
    " for future use; or, a pre-trained perceptron can be used for "
    "classification on new points.",
    // Long description.
    "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("predictions") +
    " output parameter.  The perceptron model may be saved with the " +
    PRINT_PARAM_STRING("output_model") + " output parameter."
    "\n\n"
    "Note: the following parameter is deprecated and "
    "will be removed in mlpack 4.0.0: " + PRINT_PARAM_STRING("output") +
    "."
    "\n"
    "Use " + PRINT_PARAM_STRING("predictions") + " instead of " +
    PRINT_PARAM_STRING("output") + '.' +
    "\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_model") + "."
    "\n\n" +
    PRINT_CALL("perceptron", "training", "training_data", "labels",
        "training_labels", "output_model", "perceptron_model") +
    "\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_model", "test",
        "test_data", "predictions", "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.",
    SEE_ALSO("@adaboost", "#adaboost"),
    SEE_ALSO("Perceptron on Wikipedia",
        "https://en.wikipedia.org/wiki/Perceptron"),
    SEE_ALSO("mlpack::perceptron::Perceptron C++ class documentation",
        "@doxygen/classmlpack_1_1perceptron_1_1Perceptron.html"));

// 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") is deprecated and will be removed in
PARAM_UROW_OUT("output", "The matrix in which the predicted labels for the"
    " test set will be written.", "o");
PARAM_UROW_OUT("predictions", "The matrix in which the predicted labels for the"
    " test set will be written.", "P");

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", "predictions" }, false,
      "no output will be saved");
  // "output" will be removed in mlpack 4.0.0.
  ReportIgnoredParam({{ "test", false }}, "predictions");

  // 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"))
  {
    // Get and cache the value of GetPrintableParam<mat>("training").
    std::ostringstream oss;
    oss << CLI::GetPrintableParam<mat>("training");
    std::string trainingOutput = oss.str();

    Log::Info << "Training perceptron on dataset '" << trainingOutput;
    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)
      {
        // Clean memory if needed.
        if (!CLI::HasParam("input_model"))
          delete p;

        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)
      {
        // Clean memory if needed.
        if (!CLI::HasParam("input_model"))
          delete p;

        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 '" << trainingOutput << "' 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)
    {
      // Clean memory if needed.
      const size_t perceptronDimensionality = p->P().Weights().n_rows;
      if (!CLI::HasParam("input_model"))
        delete p;

      Log::Fatal << "Test data dimensionality (" << testData.n_rows << ") must "
          << "be the same as the dimensionality of the perceptron ("
          << perceptronDimensionality << ")!" << 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") = results;
    if (CLI::HasParam("predictions"))
      CLI::GetParam<arma::Row<size_t>>("predictions") = std::move(results);
  }

  // Lastly, save the output model.
  CLI::GetParam<PerceptronModel*>("output_model") = p;
}
