IrisClassification#
In this example we will build IrisClassification.
DataSet#
we will create a Dataset to load the data and labels, IrisDataset will need to fulfill meta::is_a_dataset_v<IrisDataset>
which just asserts that IrisDataset
needs to have the following methods:
struct IrisDataset{
using TrainingFormat = std::vector<std::pair<std::vector<double>,std::vector<double>>>;
IrisDataset(std::string_view filename, bool train, std::size_t batchSize);
const std::pair<std::vector<double>&, std::vector<double>&> operator()() noexcept;
std::size_t size() const noexcept;
std::size_t getBatchSize() const noexcept;
void shuffle() noexcept;
// data
mutable std::size_t m_index;
std::size_t m_batchSize;
std::vector<std::pair<std::vector<double>,std::vector<double>>> m_data;
};
the constructor
will take care of loading the data, splitting it and one-hot encoding the target labels.
IrisDataset(std::string_view filename, bool train, std::size_t batchSize)
: m_index(0u)
, m_batchSize(batchSize){
std::string dataInput(filename);
std::fstream csv(dataInput);
auto irisData = EvoAI::readCSVFile(csv);
csv.close();
std::mt19937_64 g = EvoAI::randomGen().getEngine();
std::shuffle(std::begin(irisData), std::end(irisData), g);
std::size_t start = 0u;
auto percent = 1.0;
if(train){
percent = 0.8;
}else{
percent = 0.2;
}
std::size_t end = std::floor(irisData.size() * percent);
for(auto i=start;i<end;++i){
std::vector<double> in;
in.reserve(irisData[i].size());
std::vector<double> out;
out.reserve(3);
auto size = irisData[i].size()-1;
for(auto j=0u;j<size;++j){
in.emplace_back(std::stod(irisData[i][j]));
}
if(irisData[i][size] == "Iris-setosa"){
out.emplace_back(1.0);
out.emplace_back(0.0);
out.emplace_back(0.0);
}else if(irisData[i][size] == "Iris-versicolor"){
out.emplace_back(0.0);
out.emplace_back(1.0);
out.emplace_back(0.0);
}else if(irisData[i][size] == "Iris-virginica"){
out.emplace_back(0.0);
out.emplace_back(0.0);
out.emplace_back(1.0);
}
m_data.emplace_back(in, out);
}
}
The size
and getBatchSize
will return the sizes
std::size_t size() const noexcept{
return (m_data.size() + m_batchSize - 1) / m_batchSize;
}
std::size_t getBatchSize() const noexcept{
return m_batchSize;
}
the shuffle
method will shuffle around the data so it doesn’t repeat the same data for each batch.
void shuffle() noexcept{
auto g = EvoAI::randomGen().getEngine();
std::uniform_int_distribution ud(0, static_cast<int>(m_data.size() - 1));
for(auto i=0u;i<m_data.size();++i){
auto index1 = ud(g);
auto index2 = ud(g);
std::swap(m_data[index1], m_data[index2]);
}
}
the operator() will return an input and target to give to the NeuralNetwork::forward
and
LossFn
.
const std::pair<std::vector<double>&, std::vector<double>&> operator()() noexcept{
auto i = m_index;
m_index = (m_index + 1) % m_data.size();
return std::make_pair(std::ref(m_data[i].first), std::ref(m_data[i].second));
}
Training#
The training is just a call to nn->train
we use an Optimizer
with a learning rate of 0.1,
using SGD
and a MultiStepLR
which at step 175 will multiply the learning rate to 0.1, then we will write the data returned by
nn->train
to “irisAvgLoss.txt” so we can use python ..tools/showMultiLinePlot.py irisAvgLoss.txt
.
EvoAI::Optimizer optim(0.1, batchSize, EvoAI::SGD(m_nn->getParameters(), 0.0), EvoAI::Scheduler(EvoAI::MultiStepLR({175}, 0.1)));
EvoAI::writeMultiPlot("irisAvgLoss.txt", {"epochAvgLoss", "testAvgLoss", "accuracy"},
m_nn->train(trainingSet, testingSet, optim, epoch, EvoAI::Loss::MultiClassCrossEntropy{}, testDataset));
Evolve#
We will evolve a Population
of Genome
to use NEAT, we will create a
EvoAI::Population<EvoAI::Genome>
of 500 members with coefficients of 2.0 for excess Genes, 2.0 for
disjoints Genes and 1.0 for weight difference, the next two numbers are the Genome
constructor
which will make a Genome
of 4 inputs and 3 outputs.
The Population::setCompatibilityThreshold
is set to 10, this is the Genome::distance
between genomes to be considered another Species
.
Then we will make a lambda to easily create the IrisClassifier
and configure the ActivationType
of the hidden and output layers.
EvoAI::Population<EvoAI::Genome> p(500, 2.0, 2.0, 1.0, 4,3);
p.setCompatibilityThreshold(10.0);
auto loss = 999.0;
std::size_t gen = 0u;
auto makeIrisClass = [&normalize](EvoAI::Genome& g){
auto nn = EvoAI::Genome::makePhenotype(g);
nn[1].setActivationType(EvoAI::Neuron::ActivationType::RELU);
nn[2].setActivationType(EvoAI::Neuron::ActivationType::SOFTMAX);
return std::make_unique<IrisClassifier>(normalize, std::make_unique<EvoAI::NeuralNetwork>(std::move(nn)));
};
now we will make another lambda to evaluate the IrisClassifier
from the given Genome
.
We mutate the given Genome
and then build the IrisClassifier
to evaluate, we use the trainingSet
to get input and target and call IrisClass->forward(input)
add the loss and set the fitness of the given Genome
auto eval = [&](auto& g){
loss = 0.0;
g.mutate();
auto irisClass = makeIrisClass(g);
auto size = trainingSet.size() * trainingSet.getBatchSize();
for(auto i=0u;i<size;++i){
auto [input, target] = trainingSet();
auto outputs = irisClass->forward(input);
loss += EvoAI::Loss::MultiClassCrossEntropy{}(target, outputs);
}
loss /= size;
g.setFitness(100.0 - loss);
};
Then we will start the main loop to evaluate the members of the Population
and reproduce
the best
till the loss is less than lossThreshold
which a 0.07 will give us a 89% accuracy.
while(loss > lossThreshold){
p.eval(eval);
std::cout << "\rGeneration: " << gen << " avg fitness: " << p.computeAvgFitness() << " NumSpecies: " << p.getSpeciesSize() << " Loss: " << loss << " ";
std::flush(std::cout);
if(loss > lossThreshold){
p.reproduce(EvoAI::SelectionAlgorithms::Tournament<EvoAI::Genome>{p.getPopulationMaxSize(), 5}, true);
p.increaseAgeAndRemoveOldSpecies();
p.regrowPopulation(2.0, 2.0, 1.0, 4, 3);
}else{
std::cout << std::endl;
}
++gen;
}
auto g = p.getBestMember();
g->writeToFile("irisGenNEAT.json");
auto irisClass = makeIrisClass(*g);
irisClass->writeToFile("irisNEAT.json");
irisClass->writeDotFile("IrisNEAT.dot");
irisClass->test(testingSet);
the full code is here