Refactor perceptron program to load/save models.
Now you can save perceptrons, which is a big functionality boost. Also the documentation has been redone.
This commit is contained in:
@@ -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<typename Archive>
|
||||
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<string>("train_file");
|
||||
mat trainingData;
|
||||
data::Load(trainingDataFilename, trainingData, true);
|
||||
// First, get all parameters and validate them.
|
||||
const string trainingDataFile = CLI::GetParam<string>("training_file");
|
||||
const string labelsFile = CLI::GetParam<string>("labels_file");
|
||||
const string inputModelFile = CLI::GetParam<string>("input_model");
|
||||
const string testDataFile = CLI::GetParam<string>("test_file");
|
||||
const string outputModelFile = CLI::GetParam<string>("output_model");
|
||||
const string outputFile = CLI::GetParam<string>("output_file");
|
||||
const size_t maxIterations = (size_t) CLI::GetParam<int>("max_iterations");
|
||||
|
||||
const string labelsFilename = CLI::GetParam<string>("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<string>("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<string>("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<size_t> 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<size_t> 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<size_t> labels;
|
||||
vec mappings;
|
||||
data::NormalizeLabels(labelsIn.unsafe_col(0), labels, mappings);
|
||||
|
||||
// Load test dataset.
|
||||
const string testingDataFilename = CLI::GetParam<string>("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<int>("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<size_t> 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<string>("output");
|
||||
data::Save(outputFilename, results, true, false);
|
||||
// Clean up memory.
|
||||
delete p;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user