Split correctly the rnn tests

Signed-off-by: Omar Shrit <omar@shrit.me>
This commit is contained in:
Omar Shrit
2020-10-03 16:55:40 +02:00
parent 2cc44b2f54
commit 89d598cd79
3 changed files with 810 additions and 780 deletions
+1
View File
@@ -146,6 +146,7 @@ add_executable(mlpack_catch_test
randomized_svd_test.cpp
rbm_network_test.cpp
recurrent_network_test.cpp
rnn_rebert_test.cpp
regularized_svd_test.cpp
scaling_test.cpp
serialization_catch.cpp
+176 -780
View File
@@ -28,674 +28,6 @@ using namespace mlpack::ann;
using namespace ens;
using namespace mlpack::math;
/**
* Construct a 2-class dataset out of noisy sines.
*
* @param data Input data used to store the noisy sines.
* @param labels Labels used to store the target class of the noisy sines.
* @param points Number of points/features in a single sequence.
* @param sequences Number of sequences for each class.
* @param noise The noise factor that influences the sines.
*/
void GenerateNoisySines(arma::cube& data,
arma::mat& labels,
const size_t points,
const size_t sequences,
const double noise = 0.3)
{
arma::colvec x = arma::linspace<arma::colvec>(0, points - 1, points) /
points * 20.0;
arma::colvec y1 = arma::sin(x + arma::as_scalar(arma::randu(1)) * 3.0);
arma::colvec y2 = arma::sin(x / 2.0 + arma::as_scalar(arma::randu(1)) * 3.0);
data = arma::zeros(1 /* single dimension */, sequences * 2, points);
labels = arma::zeros(2 /* 2 classes */, sequences * 2);
for (size_t seq = 0; seq < sequences; seq++)
{
arma::vec sequence = arma::randu(points) * noise + y1 +
arma::as_scalar(arma::randu(1) - 0.5) * noise;
for (size_t i = 0; i < points; ++i)
data(0, seq, i) = sequence[i];
labels(0, seq) = 1;
sequence = arma::randu(points) * noise + y2 +
arma::as_scalar(arma::randu(1) - 0.5) * noise;
for (size_t i = 0; i < points; ++i)
data(0, sequences + seq, i) = sequence[i];
labels(1, sequences + seq) = 1;
}
}
/**
* Train the BRNN on a larger dataset.
*/
TEST_CASE("SequenceClassificationBRNNTest", "[RecurrentNetworkTest]")
{
// Using same test for RNN below.
size_t successes = 0;
const size_t rho = 10;
for (size_t trial = 0; trial < 6; ++trial)
{
// Generate 12 (2 * 6) noisy sines. A single sine contains rho
// points/features.
arma::cube input;
arma::mat labelsTemp;
GenerateNoisySines(input, labelsTemp, rho, 6);
arma::cube labels = arma::zeros<arma::cube>(1, labelsTemp.n_cols, rho);
for (size_t i = 0; i < labelsTemp.n_cols; ++i)
{
const int value = arma::as_scalar(arma::find(
arma::max(labelsTemp.col(i)) == labelsTemp.col(i), 1)) + 1;
labels.tube(0, i).fill(value);
}
Add<> add(4);
Linear<> lookup(1, 4);
SigmoidLayer<> sigmoidLayer;
Linear<> linear(4, 4);
Recurrent<>* recurrent = new Recurrent<>(
add, lookup, linear, sigmoidLayer, rho);
BRNN<> model(rho);
model.Add<IdentityLayer<> >();
model.Add(recurrent);
model.Add<Linear<> >(4, 5);
StandardSGD opt(0.1, 1, 500 * input.n_cols, -100);
model.Train(input, labels, opt);
INFO("Training over");
arma::cube prediction;
model.Predict(input, prediction);
INFO("Prediction over");
size_t error = 0;
for (size_t i = 0; i < prediction.n_cols; ++i)
{
const int predictionValue = arma::as_scalar(arma::find(
arma::max(prediction.slice(rho - 1).col(i)) ==
prediction.slice(rho - 1).col(i), 1) + 1);
const int targetValue = arma::as_scalar(arma::find(
arma::max(labelsTemp.col(i)) == labelsTemp.col(i), 1)) + 1;
if (predictionValue == targetValue)
{
error++;
}
}
double classificationError = 1 - double(error) / prediction.n_cols;
INFO(classificationError);
if (classificationError <= 0.2)
{
++successes;
break;
}
}
REQUIRE(successes >= 1);
}
/**
* Train the vanilla network on a larger dataset.
*/
TEST_CASE("SequenceClassificationTest", "[RecurrentNetworkTest]")
{
// It isn't guaranteed that the recurrent network will converge in the
// specified number of iterations using random weights. If this works 1 of 6
// times, I'm fine with that. All I want to know is that the network is able
// to escape from local minima and to solve the task.
size_t successes = 0;
const size_t rho = 10;
for (size_t trial = 0; trial < 6; ++trial)
{
// Generate 12 (2 * 6) noisy sines. A single sine contains rho
// points/features.
arma::cube input;
arma::mat labelsTemp;
GenerateNoisySines(input, labelsTemp, rho, 6);
arma::cube labels = arma::zeros<arma::cube>(1, labelsTemp.n_cols, rho);
for (size_t i = 0; i < labelsTemp.n_cols; ++i)
{
const int value = arma::as_scalar(arma::find(
arma::max(labelsTemp.col(i)) == labelsTemp.col(i), 1)) + 1;
labels.tube(0, i).fill(value);
}
/**
* Construct a network with 1 input unit, 4 hidden units and 10 output
* units. The hidden layer is connected to itself. The network structure
* looks like:
*
* Input Hidden Output
* Layer(1) Layer(4) Layer(10)
* +-----+ +-----+ +-----+
* | | | | | |
* | +------>| +------>| |
* | | ..>| | | |
* +-----+ . +--+--+ +-----+
* . .
* . .
* .......
*/
Add<> add(4);
Linear<> lookup(1, 4);
SigmoidLayer<> sigmoidLayer;
Linear<> linear(4, 4);
Recurrent<>* recurrent = new Recurrent<>(
add, lookup, linear, sigmoidLayer, rho);
RNN<> model(rho);
model.Add<IdentityLayer<> >();
model.Add(recurrent);
model.Add<Linear<> >(4, 10);
model.Add<LogSoftMax<> >();
StandardSGD opt(0.1, 1, 500 * input.n_cols, -100);
model.Train(input, labels, opt);
arma::cube prediction;
model.Predict(input, prediction);
size_t error = 0;
for (size_t i = 0; i < prediction.n_cols; ++i)
{
const int predictionValue = arma::as_scalar(arma::find(
arma::max(prediction.slice(rho - 1).col(i)) ==
prediction.slice(rho - 1).col(i), 1) + 1);
const int targetValue = arma::as_scalar(arma::find(
arma::max(labelsTemp.col(i)) == labelsTemp.col(i), 1)) + 1;
if (predictionValue == targetValue)
{
error++;
}
}
double classificationError = 1 - double(error) / prediction.n_cols;
if (classificationError <= 0.2)
{
++successes;
break;
}
}
REQUIRE(successes >= 1);
}
/**
* Generate a random Reber grammar.
*
* For more information, see the following thesis.
*
* @code
* @misc{Gers2001,
* author = {Felix Gers},
* title = {Long Short-Term Memory in Recurrent Neural Networks},
* year = {2001}
* }
* @endcode
*
* @param transitions Reber grammar transition matrix.
* @param reber The generated Reber grammar string.
*/
void GenerateReber(const arma::Mat<char>& transitions, std::string& reber)
{
size_t idx = 0;
reber = "B";
do
{
const int grammerIdx = rand() % 2;
reber += arma::as_scalar(transitions.submat(idx, grammerIdx, idx,
grammerIdx));
idx = arma::as_scalar(transitions.submat(idx, grammerIdx + 2, idx,
grammerIdx + 2)) - '0';
} while (idx != 0);
}
/**
* Generate a random recursive Reber grammar.
*
* @param transitions Recursive Reber grammar transition matrix.
* @param averageRecursion Average recursive depth of the reber grammar.
* @param maxRecursion Maximum recursive depth of reber grammar.
* @param reber The generated embedded Reber grammar string.
* @param addEnd Add ending 'E' to the generated grammar.
*/
void GenerateRecursiveReber(const arma::Mat<char>& transitions,
size_t averageRecursion,
size_t maxRecursion,
std::string& reber,
bool addEnd = true)
{
char c = (rand() % averageRecursion) == 1 ? 'P' : 'T';
if (maxRecursion == 1 || c == 'T')
{
c = 'T';
GenerateReber(transitions, reber);
}
else
{
GenerateRecursiveReber(transitions, averageRecursion, --maxRecursion,
reber, false);
}
reber = c + reber + c;
if (addEnd)
{
reber = "B" + reber + "E";
}
}
/**
* Convert a unit vector to a Reber symbol.
*
* @param translation The unit vector to be converted.
* @param symbol The converted unit vector stored as Reber symbol.
*/
template<typename MatType>
void ReberReverseTranslation(const MatType& translation, char& symbol)
{
arma::Col<char> symbols;
symbols << 'B' << 'T' << 'S' << 'X' << 'P' << 'V' << 'E' << arma::endr;
const int idx = arma::as_scalar(arma::find(translation == 1, 1, "first"));
symbol = symbols(idx);
}
/**
* Convert a Reber symbol to a unit vector.
*
* @param symbol Reber symbol to be converted.
* @param translation The converted symbol stored as unit vector.
*/
void ReberTranslation(const char symbol, arma::colvec& translation)
{
arma::Col<char> symbols;
symbols << 'B' << 'T' << 'S' << 'X' << 'P' << 'V' << 'E' << arma::endr;
const int idx = arma::as_scalar(arma::find(symbols == symbol, 1, "first"));
translation = arma::zeros<arma::colvec>(7);
translation(idx) = 1;
}
/**
* Given a Reber string, return a Reber string with all reachable next symbols.
*
* @param transitions The Reber transistion matrix.
* @param reber The Reber string used to generate all reachable next symbols.
* @param nextReber All reachable next symbols.
*/
void GenerateNextReber(const arma::Mat<char>& transitions,
const std::string& reber, std::string& nextReber)
{
size_t idx = 0;
for (size_t grammer = 1; grammer < reber.length(); grammer++)
{
const int grammerIdx = arma::as_scalar(arma::find(
transitions.row(idx) == reber[grammer], 1, "first"));
idx = arma::as_scalar(transitions.submat(idx, grammerIdx + 2, idx,
grammerIdx + 2)) - '0';
}
nextReber = arma::as_scalar(transitions.submat(idx, 0, idx, 0));
nextReber += arma::as_scalar(transitions.submat(idx, 1, idx, 1));
}
/**
* Given a recursive Reber string, return a Reber string with all
* reachable next symbols.
*
* @param transitions The Reber transistion matrix.
* @param reber The Reber string used to generate all reachable next symbols.
* @param nextReber All reachable next symbols.
*/
void GenerateNextRecursiveReber(const arma::Mat<char>& transitions,
const std::string& reber,
std::string& nextReber)
{
size_t state = 0;
size_t numPs = 0;
for (size_t cIndex = 0; cIndex < reber.length(); cIndex++)
{
char c = reber[cIndex];
if (c == 'B' && state == 0)
{
state = 1;
}
else if (c == 'P' && state == 1)
{
numPs++;
state = 1;
}
else if (c == 'T' && state == 1)
{
state = 2;
}
else if (c == 'B' && state == 2)
{
size_t pos = reber.find('E');
if (pos != std::string::npos)
{
cIndex = pos;
state = 4;
}
else
{
GenerateNextReber(transitions, reber.substr(cIndex), nextReber);
state = 3;
}
}
else if (c == 'T' && state == 4)
{
state = 5;
}
else if (c == 'P' && state == 5)
{
numPs--;
state = 5;
}
}
if (state == 0 || state == 2)
{
nextReber = "B";
}
else if (state == 1)
{
nextReber = "PT";
}
else if (state == 4)
{
nextReber = "T";
}
else if (state == 5)
{
if (numPs == 0)
{
nextReber = "E";
}
else
{
nextReber = "P";
}
}
}
/**
* @brief Creates the reber grammar data for tests.
*
* @param trainInput The train data
* @param trainLabels The train labels
* @param testInput The test input
* @param recursive whether recursive Reber
* @param trainReberGrammarCount The number of training set
* @param testReberGrammarCount The number of test set
* @param averageRecursion Average recursion
* @param maxRecursion Max recursion
* @return arma::Mat<char> The Reber state translation to be used.
*/
arma::Mat<char> GenerateReberGrammarData(
arma::field<arma::mat>& trainInput,
arma::field<arma::mat>& trainLabels,
arma::field<arma::mat>& testInput,
bool recursive = false,
const size_t trainReberGrammarCount = 700,
const size_t testReberGrammarCount = 250,
const size_t averageRecursion = 3,
const size_t maxRecursion = 5)
{
// Reber state transition matrix. (The last two columns are the indices to the
// next path).
arma::Mat<char> transitions;
transitions << 'T' << 'P' << '1' << '2' << arma::endr
<< 'X' << 'S' << '3' << '1' << arma::endr
<< 'V' << 'T' << '4' << '2' << arma::endr
<< 'X' << 'S' << '2' << '5' << arma::endr
<< 'P' << 'V' << '3' << '5' << arma::endr
<< 'E' << 'E' << '0' << '0' << arma::endr;
std::string trainReber, testReber;
arma::colvec translation;
// Generate the training data.
for (size_t i = 0; i < trainReberGrammarCount; ++i)
{
if (recursive)
GenerateRecursiveReber(transitions, 3, 5, trainReber);
else
GenerateReber(transitions, trainReber);
for (size_t j = 0; j < trainReber.length() - 1; ++j)
{
ReberTranslation(trainReber[j], translation);
trainInput(0, i) = arma::join_cols(trainInput(0, i), translation);
ReberTranslation(trainReber[j + 1], translation);
trainLabels(0, i) = arma::join_cols(trainLabels(0, i), translation);
}
}
// Generate the test data.
for (size_t i = 0; i < testReberGrammarCount; ++i)
{
if (recursive)
GenerateRecursiveReber(transitions, averageRecursion, maxRecursion,
testReber);
else
GenerateReber(transitions, testReber);
for (size_t j = 0; j < testReber.length() - 1; ++j)
{
ReberTranslation(testReber[j], translation);
testInput(0, i) = arma::join_cols(testInput(0, i), translation);
}
}
return transitions;
}
/**
* Train the specified network and the construct a Reber grammar dataset.
*/
template<typename ModelType>
void ReberGrammarTestNetwork(ModelType& model,
const bool recursive = false,
const size_t averageRecursion = 3,
const size_t maxRecursion = 5,
const size_t iterations = 10,
const size_t trials = 5)
{
const size_t trainReberGrammarCount = 700;
const size_t testReberGrammarCount = 250;
arma::field<arma::mat> trainInput(1, trainReberGrammarCount);
arma::field<arma::mat> trainLabels(1, trainReberGrammarCount);
arma::field<arma::mat> testInput(1, testReberGrammarCount);
arma::Mat<char> transitions =
GenerateReberGrammarData(trainInput,
trainLabels,
testInput,
recursive,
trainReberGrammarCount,
testReberGrammarCount,
averageRecursion,
maxRecursion);
/*
* Construct a network with 7 input units, layerSize hidden units and 7 output
* units. The hidden layer is connected to itself. The network structure looks
* like:
*
* Input Hidden Output
* Layer(7) Layer(layerSize) Layer(7)
* +-----+ +-----+ +-----+
* | | | | | |
* | +------>| +------>| |
* | | ..>| | | |
* +-----+ . +--+--+ +-- ---+
* . .
* . .
* .......
*/
// It isn't guaranteed that the recurrent network will converge in the
// specified number of iterations using random weights. If this works 1 of 5
// times, I'm fine with that. All I want to know is that the network is able
// to escape from local minima and to solve the task.
size_t successes = 0;
size_t offset = 0;
const size_t inputSize = 7;
for (size_t trial = 0; trial < trials; ++trial)
{
// Reset model before using for next trial.
model.Reset();
MomentumSGD opt(0.06, 50, 2, -50000);
arma::cube inputTemp, labelsTemp;
for (size_t iteration = 0; iteration < (iterations + offset); iteration++)
{
for (size_t j = 0; j < trainReberGrammarCount; ++j)
{
// Each sequence may be a different length, so we need to extract them
// manually. We will reshape them into a cube with each slice equal to
// a time step.
inputTemp = arma::cube(trainInput.at(0, j).memptr(), inputSize, 1,
trainInput.at(0, j).n_elem / inputSize, false, true);
labelsTemp = arma::cube(trainLabels.at(0, j).memptr(), inputSize, 1,
trainInput.at(0, j).n_elem / inputSize, false, true);
model.Rho() = inputTemp.n_elem / inputSize;
model.Train(inputTemp, labelsTemp, opt);
opt.ResetPolicy() = false;
}
}
double error = 0;
// Ask the network to predict the next Reber grammar in the given sequence.
for (size_t i = 0; i < testReberGrammarCount; ++i)
{
arma::cube prediction;
arma::cube input(testInput.at(0, i).memptr(), inputSize, 1,
testInput.at(0, i).n_elem / inputSize, false, true);
model.Rho() = input.n_elem / inputSize;
model.Predict(input, prediction);
const size_t reberGrammerSize = 7;
std::string inputReber = "";
size_t reberError = 0;
for (size_t j = 0; j < (prediction.n_elem / reberGrammerSize); ++j)
{
char predictedSymbol, inputSymbol;
std::string reberChoices;
arma::umat output = (prediction.slice(j) == (arma::ones(
reberGrammerSize, 1) *
arma::as_scalar(arma::max(prediction.slice(j)))));
ReberReverseTranslation(output, predictedSymbol);
ReberReverseTranslation(input.slice(j), inputSymbol);
inputReber += inputSymbol;
if (recursive)
GenerateNextRecursiveReber(transitions, inputReber, reberChoices);
else
GenerateNextReber(transitions, inputReber, reberChoices);
if (reberChoices.find(predictedSymbol) != std::string::npos)
reberError++;
}
if (reberError != (prediction.n_elem / reberGrammerSize))
error += 1;
}
error /= testReberGrammarCount;
if (error <= 0.3)
{
++successes;
break;
}
offset += 3;
}
REQUIRE(successes >= 1);
}
/**
* Train the specified networks on an embedded Reber grammar dataset.
*/
TEST_CASE("LSTMReberGrammarTest", "[RecurrentNetworkTest]")
{
RNN<MeanSquaredError<> > model(5);
model.Add<Linear<> >(7, 10);
model.Add<LSTM<> >(10, 10);
model.Add<Linear<> >(10, 7);
model.Add<SigmoidLayer<> >();
ReberGrammarTestNetwork(model, false);
}
/**
* Train the specified networks on an embedded Reber grammar dataset.
*/
TEST_CASE("FastLSTMReberGrammarTest", "[RecurrentNetworkTest]")
{
RNN<MeanSquaredError<> > model(5);
model.Add<Linear<> >(7, 8);
model.Add<FastLSTM<> >(8, 8);
model.Add<Linear<> >(8, 7);
model.Add<SigmoidLayer<> >();
ReberGrammarTestNetwork(model, false);
}
/**
* Train the specified networks on an embedded Reber grammar dataset.
*/
TEST_CASE("GRURecursiveReberGrammarTest", "[RecurrentNetworkTest]")
{
RNN<MeanSquaredError<> > model(5);
model.Add<Linear<> >(7, 16);
model.Add<GRU<> >(16, 16);
model.Add<Linear<> >(16, 7);
model.Add<SigmoidLayer<> >();
ReberGrammarTestNetwork(model, true, 3, 5, 10, 7);
}
/**
* Train BLSTM on an embedded Reber grammar dataset.
*/
TEST_CASE("BRNNReberGrammarTest", "[RecurrentNetworkTest]")
{
BRNN<MeanSquaredError<>, AddMerge<>, SigmoidLayer<> > model(5);
model.Add<Linear<> >(7, 10);
model.Add<LSTM<> >(10, 10);
model.Add<Linear<> >(10, 7);
ReberGrammarTestNetwork(model, false, 3, 5, 1);
}
/*
* This sample is a simplified version of Derek D. Monner's Distracted Sequence
* Recall task, which involves 10 symbols:
@@ -1042,142 +374,206 @@ TEST_CASE("RNNSerializationTest", "[RecurrentNetworkTest]")
}
/**
* Test RNN with a custom layer.
* Construct a 2-class dataset out of noisy sines.
*
* @param data Input data used to store the noisy sines.
* @param labels Labels used to store the target class of the noisy sines.
* @param points Number of points/features in a single sequence.
* @param sequences Number of sequences for each class.
* @param noise The noise factor that influences the sines.
*/
void ReberGrammarTestCustomNetwork(const size_t hiddenSize = 4,
const bool recursive = false,
const size_t iterations = 10)
void GenerateNoisySines(arma::cube& data,
arma::mat& labels,
const size_t points,
const size_t sequences,
const double noise = 0.3)
{
const size_t trainReberGrammarCount = 700;
const size_t testReberGrammarCount = 250;
arma::colvec x = arma::linspace<arma::colvec>(0, points - 1, points) /
points * 20.0;
arma::colvec y1 = arma::sin(x + arma::as_scalar(arma::randu(1)) * 3.0);
arma::colvec y2 = arma::sin(x / 2.0 + arma::as_scalar(arma::randu(1)) * 3.0);
arma::field<arma::mat> trainInput(1, trainReberGrammarCount);
arma::field<arma::mat> trainLabels(1, trainReberGrammarCount);
arma::field<arma::mat> testInput(1, testReberGrammarCount);
data = arma::zeros(1 /* single dimension */, sequences * 2, points);
labels = arma::zeros(2 /* 2 classes */, sequences * 2);
arma::Mat<char> transitions =
GenerateReberGrammarData(trainInput,
trainLabels,
testInput,
recursive,
trainReberGrammarCount,
testReberGrammarCount);
/*
* Construct a network with 7 input units, layerSize hidden units and 7 output
* units. The hidden layer is connected to itself. The network structure looks
* like:
*
* Input Hidden Output
* Layer(7) Layer(layerSize) Layer(7)
* +-----+ +-----+ +-----+
* | | | | | |
* | +------>| +------>| |
* | | ..>| | | |
* +-----+ . +--+--+ +-- ---+
* . .
* . .
* .......
*/
// It isn't guaranteed that the recurrent network will converge in the
// specified number of iterations using random weights. If this works 1 of 10
// times, I'm fine with that. All I want to know is that the network is able
// to escape from local minima and to solve the task.
size_t successes = 0;
size_t offset = 0;
for (size_t trial = 0; trial < 10; ++trial)
for (size_t seq = 0; seq < sequences; seq++)
{
const size_t outputSize = 7;
const size_t inputSize = 7;
arma::vec sequence = arma::randu(points) * noise + y1 +
arma::as_scalar(arma::randu(1) - 0.5) * noise;
for (size_t i = 0; i < points; ++i)
data(0, seq, i) = sequence[i];
RNN<MeanSquaredError<>, RandomInitialization, CustomLayer<> > model(5);
model.Add<Linear<> >(inputSize, hiddenSize);
model.Add<GRU<> >(hiddenSize, hiddenSize);
model.Add<Linear<> >(hiddenSize, outputSize);
model.Add<CustomLayer<> >();
MomentumSGD opt(0.06, 50, 2, -50000);
labels(0, seq) = 1;
arma::cube inputTemp, labelsTemp;
for (size_t iteration = 0; iteration < (iterations + offset); iteration++)
sequence = arma::randu(points) * noise + y2 +
arma::as_scalar(arma::randu(1) - 0.5) * noise;
for (size_t i = 0; i < points; ++i)
data(0, sequences + seq, i) = sequence[i];
labels(1, sequences + seq) = 1;
}
}
/**
* Train the BRNN on a larger dataset.
*/
TEST_CASE("SequenceClassificationBRNNTest", "[RecurrentNetworkTest]")
{
// Using same test for RNN below.
size_t successes = 0;
const size_t rho = 10;
for (size_t trial = 0; trial < 6; ++trial)
{
// Generate 12 (2 * 6) noisy sines. A single sine contains rho
// points/features.
arma::cube input;
arma::mat labelsTemp;
GenerateNoisySines(input, labelsTemp, rho, 6);
arma::cube labels = arma::zeros<arma::cube>(1, labelsTemp.n_cols, rho);
for (size_t i = 0; i < labelsTemp.n_cols; ++i)
{
for (size_t j = 0; j < trainReberGrammarCount; ++j)
{
// Each sequence may be a different length, so we need to extract them
// manually. We will reshape them into a cube with each slice equal to
// a time step.
inputTemp = arma::cube(trainInput.at(0, j).memptr(), inputSize, 1,
trainInput.at(0, j).n_elem / inputSize, false, true);
labelsTemp = arma::cube(trainLabels.at(0, j).memptr(), inputSize, 1,
trainInput.at(0, j).n_elem / inputSize, false, true);
const int value = arma::as_scalar(arma::find(
arma::max(labelsTemp.col(i)) == labelsTemp.col(i), 1)) + 1;
labels.tube(0, i).fill(value);
}
model.Rho() = inputTemp.n_elem / inputSize;
model.Train(inputTemp, labelsTemp, opt);
opt.ResetPolicy() = false;
Add<> add(4);
Linear<> lookup(1, 4);
SigmoidLayer<> sigmoidLayer;
Linear<> linear(4, 4);
Recurrent<>* recurrent = new Recurrent<>(
add, lookup, linear, sigmoidLayer, rho);
BRNN<> model(rho);
model.Add<IdentityLayer<> >();
model.Add(recurrent);
model.Add<Linear<> >(4, 5);
StandardSGD opt(0.1, 1, 500 * input.n_cols, -100);
model.Train(input, labels, opt);
INFO("Training over");
arma::cube prediction;
model.Predict(input, prediction);
INFO("Prediction over");
size_t error = 0;
for (size_t i = 0; i < prediction.n_cols; ++i)
{
const int predictionValue = arma::as_scalar(arma::find(
arma::max(prediction.slice(rho - 1).col(i)) ==
prediction.slice(rho - 1).col(i), 1) + 1);
const int targetValue = arma::as_scalar(arma::find(
arma::max(labelsTemp.col(i)) == labelsTemp.col(i), 1)) + 1;
if (predictionValue == targetValue)
{
error++;
}
}
double error = 0;
// Ask the network to predict the next Reber grammar in the given sequence.
for (size_t i = 0; i < testReberGrammarCount; ++i)
{
arma::cube prediction;
arma::cube input(testInput.at(0, i).memptr(), inputSize, 1,
testInput.at(0, i).n_elem / inputSize, false, true);
model.Rho() = input.n_elem / inputSize;
model.Predict(input, prediction);
const size_t reberGrammerSize = 7;
std::string inputReber = "";
size_t reberError = 0;
for (size_t j = 0; j < (prediction.n_elem / reberGrammerSize); ++j)
{
char predictedSymbol, inputSymbol;
std::string reberChoices;
arma::umat output = (prediction.slice(j) == (arma::ones(
reberGrammerSize, 1) *
arma::as_scalar(arma::max(prediction.slice(j)))));
ReberReverseTranslation(output, predictedSymbol);
ReberReverseTranslation(input.slice(j), inputSymbol);
inputReber += inputSymbol;
if (recursive)
GenerateNextRecursiveReber(transitions, inputReber, reberChoices);
else
GenerateNextReber(transitions, inputReber, reberChoices);
if (reberChoices.find(predictedSymbol) != std::string::npos)
reberError++;
}
if (reberError != (prediction.n_elem / reberGrammerSize))
error += 1;
}
error /= testReberGrammarCount;
if (error <= 0.35)
double classificationError = 1 - double(error) / prediction.n_cols;
INFO(classificationError);
if (classificationError <= 0.2)
{
++successes;
break;
}
offset += 3;
}
REQUIRE(successes >= 1);
}
/**
* Train the specified networks on an embedded Reber grammar dataset.
* Train the vanilla network on a larger dataset.
*/
TEST_CASE("CustomRecursiveReberGrammarTest", "[RecurrentNetworkTest]")
TEST_CASE("SequenceClassificationTest", "[RecurrentNetworkTest]")
{
ReberGrammarTestCustomNetwork(16, true);
// It isn't guaranteed that the recurrent network will converge in the
// specified number of iterations using random weights. If this works 1 of 6
// times, I'm fine with that. All I want to know is that the network is able
// to escape from local minima and to solve the task.
size_t successes = 0;
const size_t rho = 10;
for (size_t trial = 0; trial < 6; ++trial)
{
// Generate 12 (2 * 6) noisy sines. A single sine contains rho
// points/features.
arma::cube input;
arma::mat labelsTemp;
GenerateNoisySines(input, labelsTemp, rho, 6);
arma::cube labels = arma::zeros<arma::cube>(1, labelsTemp.n_cols, rho);
for (size_t i = 0; i < labelsTemp.n_cols; ++i)
{
const int value = arma::as_scalar(arma::find(
arma::max(labelsTemp.col(i)) == labelsTemp.col(i), 1)) + 1;
labels.tube(0, i).fill(value);
}
/**
* Construct a network with 1 input unit, 4 hidden units and 10 output
* units. The hidden layer is connected to itself. The network structure
* looks like:
*
* Input Hidden Output
* Layer(1) Layer(4) Layer(10)
* +-----+ +-----+ +-----+
* | | | | | |
* | +------>| +------>| |
* | | ..>| | | |
* +-----+ . +--+--+ +-----+
* . .
* . .
* .......
*/
Add<> add(4);
Linear<> lookup(1, 4);
SigmoidLayer<> sigmoidLayer;
Linear<> linear(4, 4);
Recurrent<>* recurrent = new Recurrent<>(
add, lookup, linear, sigmoidLayer, rho);
RNN<> model(rho);
model.Add<IdentityLayer<> >();
model.Add(recurrent);
model.Add<Linear<> >(4, 10);
model.Add<LogSoftMax<> >();
StandardSGD opt(0.1, 1, 500 * input.n_cols, -100);
model.Train(input, labels, opt);
arma::cube prediction;
model.Predict(input, prediction);
size_t error = 0;
for (size_t i = 0; i < prediction.n_cols; ++i)
{
const int predictionValue = arma::as_scalar(arma::find(
arma::max(prediction.slice(rho - 1).col(i)) ==
prediction.slice(rho - 1).col(i), 1) + 1);
const int targetValue = arma::as_scalar(arma::find(
arma::max(labelsTemp.col(i)) == labelsTemp.col(i), 1)) + 1;
if (predictionValue == targetValue)
{
error++;
}
}
double classificationError = 1 - double(error) / prediction.n_cols;
if (classificationError <= 0.2)
{
++successes;
break;
}
}
REQUIRE(successes >= 1);
}
/**
+633
View File
@@ -0,0 +1,633 @@
/**
* @file tests/recurrent_network_test.cpp
* @author Marcus Edel
*
* Tests the recurrent network.
*
* 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/core.hpp>
#include <ensmallen.hpp>
#include <mlpack/methods/ann/layer/layer.hpp>
#include <mlpack/methods/ann/loss_functions/mean_squared_error.hpp>
#include <mlpack/methods/ann/rnn.hpp>
#include <mlpack/methods/ann/brnn.hpp>
#include <mlpack/core/data/binarize.hpp>
#include <mlpack/core/math/random.hpp>
#include "catch.hpp"
#include "serialization_catch.hpp"
#include "custom_layer.hpp"
using namespace mlpack;
using namespace mlpack::ann;
using namespace ens;
using namespace mlpack::math;
/**
* Generate a random Reber grammar.
*
* For more information, see the following thesis.
*
* @code
* @misc{Gers2001,
* author = {Felix Gers},
* title = {Long Short-Term Memory in Recurrent Neural Networks},
* year = {2001}
* }
* @endcode
*
* @param transitions Reber grammar transition matrix.
* @param reber The generated Reber grammar string.
*/
void GenerateReber(const arma::Mat<char>& transitions, std::string& reber)
{
size_t idx = 0;
reber = "B";
do
{
const int grammerIdx = rand() % 2;
reber += arma::as_scalar(transitions.submat(idx, grammerIdx, idx,
grammerIdx));
idx = arma::as_scalar(transitions.submat(idx, grammerIdx + 2, idx,
grammerIdx + 2)) - '0';
} while (idx != 0);
}
/**
* Generate a random recursive Reber grammar.
*
* @param transitions Recursive Reber grammar transition matrix.
* @param averageRecursion Average recursive depth of the reber grammar.
* @param maxRecursion Maximum recursive depth of reber grammar.
* @param reber The generated embedded Reber grammar string.
* @param addEnd Add ending 'E' to the generated grammar.
*/
void GenerateRecursiveReber(const arma::Mat<char>& transitions,
size_t averageRecursion,
size_t maxRecursion,
std::string& reber,
bool addEnd = true)
{
char c = (rand() % averageRecursion) == 1 ? 'P' : 'T';
if (maxRecursion == 1 || c == 'T')
{
c = 'T';
GenerateReber(transitions, reber);
}
else
{
GenerateRecursiveReber(transitions, averageRecursion, --maxRecursion,
reber, false);
}
reber = c + reber + c;
if (addEnd)
{
reber = "B" + reber + "E";
}
}
/**
* Convert a unit vector to a Reber symbol.
*
* @param translation The unit vector to be converted.
* @param symbol The converted unit vector stored as Reber symbol.
*/
template<typename MatType>
void ReberReverseTranslation(const MatType& translation, char& symbol)
{
arma::Col<char> symbols;
symbols << 'B' << 'T' << 'S' << 'X' << 'P' << 'V' << 'E' << arma::endr;
const int idx = arma::as_scalar(arma::find(translation == 1, 1, "first"));
symbol = symbols(idx);
}
/**
* Convert a Reber symbol to a unit vector.
*
* @param symbol Reber symbol to be converted.
* @param translation The converted symbol stored as unit vector.
*/
void ReberTranslation(const char symbol, arma::colvec& translation)
{
arma::Col<char> symbols;
symbols << 'B' << 'T' << 'S' << 'X' << 'P' << 'V' << 'E' << arma::endr;
const int idx = arma::as_scalar(arma::find(symbols == symbol, 1, "first"));
translation = arma::zeros<arma::colvec>(7);
translation(idx) = 1;
}
/**
* Given a Reber string, return a Reber string with all reachable next symbols.
*
* @param transitions The Reber transistion matrix.
* @param reber The Reber string used to generate all reachable next symbols.
* @param nextReber All reachable next symbols.
*/
void GenerateNextReber(const arma::Mat<char>& transitions,
const std::string& reber, std::string& nextReber)
{
size_t idx = 0;
for (size_t grammer = 1; grammer < reber.length(); grammer++)
{
const int grammerIdx = arma::as_scalar(arma::find(
transitions.row(idx) == reber[grammer], 1, "first"));
idx = arma::as_scalar(transitions.submat(idx, grammerIdx + 2, idx,
grammerIdx + 2)) - '0';
}
nextReber = arma::as_scalar(transitions.submat(idx, 0, idx, 0));
nextReber += arma::as_scalar(transitions.submat(idx, 1, idx, 1));
}
/**
* Given a recursive Reber string, return a Reber string with all
* reachable next symbols.
*
* @param transitions The Reber transistion matrix.
* @param reber The Reber string used to generate all reachable next symbols.
* @param nextReber All reachable next symbols.
*/
void GenerateNextRecursiveReber(const arma::Mat<char>& transitions,
const std::string& reber,
std::string& nextReber)
{
size_t state = 0;
size_t numPs = 0;
for (size_t cIndex = 0; cIndex < reber.length(); cIndex++)
{
char c = reber[cIndex];
if (c == 'B' && state == 0)
{
state = 1;
}
else if (c == 'P' && state == 1)
{
numPs++;
state = 1;
}
else if (c == 'T' && state == 1)
{
state = 2;
}
else if (c == 'B' && state == 2)
{
size_t pos = reber.find('E');
if (pos != std::string::npos)
{
cIndex = pos;
state = 4;
}
else
{
GenerateNextReber(transitions, reber.substr(cIndex), nextReber);
state = 3;
}
}
else if (c == 'T' && state == 4)
{
state = 5;
}
else if (c == 'P' && state == 5)
{
numPs--;
state = 5;
}
}
if (state == 0 || state == 2)
{
nextReber = "B";
}
else if (state == 1)
{
nextReber = "PT";
}
else if (state == 4)
{
nextReber = "T";
}
else if (state == 5)
{
if (numPs == 0)
{
nextReber = "E";
}
else
{
nextReber = "P";
}
}
}
/**
* @brief Creates the reber grammar data for tests.
*
* @param trainInput The train data
* @param trainLabels The train labels
* @param testInput The test input
* @param recursive whether recursive Reber
* @param trainReberGrammarCount The number of training set
* @param testReberGrammarCount The number of test set
* @param averageRecursion Average recursion
* @param maxRecursion Max recursion
* @return arma::Mat<char> The Reber state translation to be used.
*/
arma::Mat<char> GenerateReberGrammarData(
arma::field<arma::mat>& trainInput,
arma::field<arma::mat>& trainLabels,
arma::field<arma::mat>& testInput,
bool recursive = false,
const size_t trainReberGrammarCount = 700,
const size_t testReberGrammarCount = 250,
const size_t averageRecursion = 3,
const size_t maxRecursion = 5)
{
// Reber state transition matrix. (The last two columns are the indices to the
// next path).
arma::Mat<char> transitions;
transitions << 'T' << 'P' << '1' << '2' << arma::endr
<< 'X' << 'S' << '3' << '1' << arma::endr
<< 'V' << 'T' << '4' << '2' << arma::endr
<< 'X' << 'S' << '2' << '5' << arma::endr
<< 'P' << 'V' << '3' << '5' << arma::endr
<< 'E' << 'E' << '0' << '0' << arma::endr;
std::string trainReber, testReber;
arma::colvec translation;
// Generate the training data.
for (size_t i = 0; i < trainReberGrammarCount; ++i)
{
if (recursive)
GenerateRecursiveReber(transitions, 3, 5, trainReber);
else
GenerateReber(transitions, trainReber);
for (size_t j = 0; j < trainReber.length() - 1; ++j)
{
ReberTranslation(trainReber[j], translation);
trainInput(0, i) = arma::join_cols(trainInput(0, i), translation);
ReberTranslation(trainReber[j + 1], translation);
trainLabels(0, i) = arma::join_cols(trainLabels(0, i), translation);
}
}
// Generate the test data.
for (size_t i = 0; i < testReberGrammarCount; ++i)
{
if (recursive)
GenerateRecursiveReber(transitions, averageRecursion, maxRecursion,
testReber);
else
GenerateReber(transitions, testReber);
for (size_t j = 0; j < testReber.length() - 1; ++j)
{
ReberTranslation(testReber[j], translation);
testInput(0, i) = arma::join_cols(testInput(0, i), translation);
}
}
return transitions;
}
/**
* Train the specified network and the construct a Reber grammar dataset.
*/
template<typename ModelType>
void ReberGrammarTestNetwork(ModelType& model,
const bool recursive = false,
const size_t averageRecursion = 3,
const size_t maxRecursion = 5,
const size_t iterations = 10,
const size_t trials = 5)
{
const size_t trainReberGrammarCount = 700;
const size_t testReberGrammarCount = 250;
arma::field<arma::mat> trainInput(1, trainReberGrammarCount);
arma::field<arma::mat> trainLabels(1, trainReberGrammarCount);
arma::field<arma::mat> testInput(1, testReberGrammarCount);
arma::Mat<char> transitions =
GenerateReberGrammarData(trainInput,
trainLabels,
testInput,
recursive,
trainReberGrammarCount,
testReberGrammarCount,
averageRecursion,
maxRecursion);
/*
* Construct a network with 7 input units, layerSize hidden units and 7 output
* units. The hidden layer is connected to itself. The network structure looks
* like:
*
* Input Hidden Output
* Layer(7) Layer(layerSize) Layer(7)
* +-----+ +-----+ +-----+
* | | | | | |
* | +------>| +------>| |
* | | ..>| | | |
* +-----+ . +--+--+ +-- ---+
* . .
* . .
* .......
*/
// It isn't guaranteed that the recurrent network will converge in the
// specified number of iterations using random weights. If this works 1 of 5
// times, I'm fine with that. All I want to know is that the network is able
// to escape from local minima and to solve the task.
size_t successes = 0;
size_t offset = 0;
const size_t inputSize = 7;
for (size_t trial = 0; trial < trials; ++trial)
{
// Reset model before using for next trial.
model.Reset();
MomentumSGD opt(0.06, 50, 2, -50000);
arma::cube inputTemp, labelsTemp;
for (size_t iteration = 0; iteration < (iterations + offset); iteration++)
{
for (size_t j = 0; j < trainReberGrammarCount; ++j)
{
// Each sequence may be a different length, so we need to extract them
// manually. We will reshape them into a cube with each slice equal to
// a time step.
inputTemp = arma::cube(trainInput.at(0, j).memptr(), inputSize, 1,
trainInput.at(0, j).n_elem / inputSize, false, true);
labelsTemp = arma::cube(trainLabels.at(0, j).memptr(), inputSize, 1,
trainInput.at(0, j).n_elem / inputSize, false, true);
model.Rho() = inputTemp.n_elem / inputSize;
model.Train(inputTemp, labelsTemp, opt);
opt.ResetPolicy() = false;
}
}
double error = 0;
// Ask the network to predict the next Reber grammar in the given sequence.
for (size_t i = 0; i < testReberGrammarCount; ++i)
{
arma::cube prediction;
arma::cube input(testInput.at(0, i).memptr(), inputSize, 1,
testInput.at(0, i).n_elem / inputSize, false, true);
model.Rho() = input.n_elem / inputSize;
model.Predict(input, prediction);
const size_t reberGrammerSize = 7;
std::string inputReber = "";
size_t reberError = 0;
for (size_t j = 0; j < (prediction.n_elem / reberGrammerSize); ++j)
{
char predictedSymbol, inputSymbol;
std::string reberChoices;
arma::umat output = (prediction.slice(j) == (arma::ones(
reberGrammerSize, 1) *
arma::as_scalar(arma::max(prediction.slice(j)))));
ReberReverseTranslation(output, predictedSymbol);
ReberReverseTranslation(input.slice(j), inputSymbol);
inputReber += inputSymbol;
if (recursive)
GenerateNextRecursiveReber(transitions, inputReber, reberChoices);
else
GenerateNextReber(transitions, inputReber, reberChoices);
if (reberChoices.find(predictedSymbol) != std::string::npos)
reberError++;
}
if (reberError != (prediction.n_elem / reberGrammerSize))
error += 1;
}
error /= testReberGrammarCount;
if (error <= 0.3)
{
++successes;
break;
}
offset += 3;
}
REQUIRE(successes >= 1);
}
/**
* Train the specified networks on an embedded Reber grammar dataset.
*/
TEST_CASE("LSTMReberGrammarTest", "[RecurrentNetworkTest]")
{
RNN<MeanSquaredError<> > model(5);
model.Add<Linear<> >(7, 10);
model.Add<LSTM<> >(10, 10);
model.Add<Linear<> >(10, 7);
model.Add<SigmoidLayer<> >();
ReberGrammarTestNetwork(model, false);
}
/**
* Train the specified networks on an embedded Reber grammar dataset.
*/
TEST_CASE("FastLSTMReberGrammarTest", "[RecurrentNetworkTest]")
{
RNN<MeanSquaredError<> > model(5);
model.Add<Linear<> >(7, 8);
model.Add<FastLSTM<> >(8, 8);
model.Add<Linear<> >(8, 7);
model.Add<SigmoidLayer<> >();
ReberGrammarTestNetwork(model, false);
}
/**
* Train the specified networks on an embedded Reber grammar dataset.
*/
TEST_CASE("GRURecursiveReberGrammarTest", "[RecurrentNetworkTest]")
{
RNN<MeanSquaredError<> > model(5);
model.Add<Linear<> >(7, 16);
model.Add<GRU<> >(16, 16);
model.Add<Linear<> >(16, 7);
model.Add<SigmoidLayer<> >();
ReberGrammarTestNetwork(model, true, 3, 5, 10, 7);
}
/**
* Train BLSTM on an embedded Reber grammar dataset.
*/
TEST_CASE("BRNNReberGrammarTest", "[RecurrentNetworkTest]")
{
BRNN<MeanSquaredError<>, AddMerge<>, SigmoidLayer<> > model(5);
model.Add<Linear<> >(7, 10);
model.Add<LSTM<> >(10, 10);
model.Add<Linear<> >(10, 7);
ReberGrammarTestNetwork(model, false, 3, 5, 1);
}
/**
* Test RNN with a custom layer.
*/
void ReberGrammarTestCustomNetwork(const size_t hiddenSize = 4,
const bool recursive = false,
const size_t iterations = 10)
{
const size_t trainReberGrammarCount = 700;
const size_t testReberGrammarCount = 250;
arma::field<arma::mat> trainInput(1, trainReberGrammarCount);
arma::field<arma::mat> trainLabels(1, trainReberGrammarCount);
arma::field<arma::mat> testInput(1, testReberGrammarCount);
arma::Mat<char> transitions =
GenerateReberGrammarData(trainInput,
trainLabels,
testInput,
recursive,
trainReberGrammarCount,
testReberGrammarCount);
/*
* Construct a network with 7 input units, layerSize hidden units and 7 output
* units. The hidden layer is connected to itself. The network structure looks
* like:
*
* Input Hidden Output
* Layer(7) Layer(layerSize) Layer(7)
* +-----+ +-----+ +-----+
* | | | | | |
* | +------>| +------>| |
* | | ..>| | | |
* +-----+ . +--+--+ +-- ---+
* . .
* . .
* .......
*/
// It isn't guaranteed that the recurrent network will converge in the
// specified number of iterations using random weights. If this works 1 of 10
// times, I'm fine with that. All I want to know is that the network is able
// to escape from local minima and to solve the task.
size_t successes = 0;
size_t offset = 0;
for (size_t trial = 0; trial < 10; ++trial)
{
const size_t outputSize = 7;
const size_t inputSize = 7;
RNN<MeanSquaredError<>, RandomInitialization, CustomLayer<> > model(5);
model.Add<Linear<> >(inputSize, hiddenSize);
model.Add<GRU<> >(hiddenSize, hiddenSize);
model.Add<Linear<> >(hiddenSize, outputSize);
model.Add<CustomLayer<> >();
MomentumSGD opt(0.06, 50, 2, -50000);
arma::cube inputTemp, labelsTemp;
for (size_t iteration = 0; iteration < (iterations + offset); iteration++)
{
for (size_t j = 0; j < trainReberGrammarCount; ++j)
{
// Each sequence may be a different length, so we need to extract them
// manually. We will reshape them into a cube with each slice equal to
// a time step.
inputTemp = arma::cube(trainInput.at(0, j).memptr(), inputSize, 1,
trainInput.at(0, j).n_elem / inputSize, false, true);
labelsTemp = arma::cube(trainLabels.at(0, j).memptr(), inputSize, 1,
trainInput.at(0, j).n_elem / inputSize, false, true);
model.Rho() = inputTemp.n_elem / inputSize;
model.Train(inputTemp, labelsTemp, opt);
opt.ResetPolicy() = false;
}
}
double error = 0;
// Ask the network to predict the next Reber grammar in the given sequence.
for (size_t i = 0; i < testReberGrammarCount; ++i)
{
arma::cube prediction;
arma::cube input(testInput.at(0, i).memptr(), inputSize, 1,
testInput.at(0, i).n_elem / inputSize, false, true);
model.Rho() = input.n_elem / inputSize;
model.Predict(input, prediction);
const size_t reberGrammerSize = 7;
std::string inputReber = "";
size_t reberError = 0;
for (size_t j = 0; j < (prediction.n_elem / reberGrammerSize); ++j)
{
char predictedSymbol, inputSymbol;
std::string reberChoices;
arma::umat output = (prediction.slice(j) == (arma::ones(
reberGrammerSize, 1) *
arma::as_scalar(arma::max(prediction.slice(j)))));
ReberReverseTranslation(output, predictedSymbol);
ReberReverseTranslation(input.slice(j), inputSymbol);
inputReber += inputSymbol;
if (recursive)
GenerateNextRecursiveReber(transitions, inputReber, reberChoices);
else
GenerateNextReber(transitions, inputReber, reberChoices);
if (reberChoices.find(predictedSymbol) != std::string::npos)
reberError++;
}
if (reberError != (prediction.n_elem / reberGrammerSize))
error += 1;
}
error /= testReberGrammarCount;
if (error <= 0.35)
{
++successes;
break;
}
offset += 3;
}
REQUIRE(successes >= 1);
}
/**
* Train the specified networks on an embedded Reber grammar dataset.
*/
TEST_CASE("CustomRecursiveReberGrammarTest", "[RecurrentNetworkTest]")
{
ReberGrammarTestCustomNetwork(16, true);
}