diff --git a/configure.ac b/configure.ac
index 1e00f7e1..69669837 100644
--- a/configure.ac
+++ b/configure.ac
@@ -80,7 +80,8 @@ AC_CONFIG_FILES([Makefile
src/Makefile
test/Makefile
tools/Makefile
- tools/metadata/Makefile])
+ tools/metadata/Makefile
+ tools/classifier/Makefile])
AC_OUTPUT
diff --git a/src/classifier/DataNormalizer.cpp b/src/classifier/DataNormalizer.cpp
new file mode 100644
index 00000000..38e85d05
--- /dev/null
+++ b/src/classifier/DataNormalizer.cpp
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2018 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include "DataNormalizer.hpp"
+
+#include
+
+#include
+
+namespace SOM
+{
+
+DataNormalizer::DataNormalizer(std::size_t inputDimCount)
+: _inputDimCount(inputDimCount)
+{
+}
+
+
+void
+DataNormalizer::computeNormalizationFactors(const std::vector& inputVectors)
+{
+ // For each dimension of the input, compute the min/max
+ _minmax.clear();
+ _minmax.resize(_inputDimCount);
+
+ for (std::size_t dimId = 0; dimId < _inputDimCount; ++dimId)
+ {
+ std::vector values;
+
+ for (const auto& inputVector: inputVectors)
+ {
+ checkSameDimensions(inputVector, _inputDimCount);
+ values.push_back(inputVector[dimId]);
+ }
+
+ auto result = std::minmax_element(values.begin(), values.end());
+ _minmax[dimId] = {*result.first, *result.second};
+ }
+}
+
+void
+DataNormalizer::normalizeData(InputVector& a) const
+{
+ checkSameDimensions(a, _inputDimCount);
+
+ for (std::size_t dimId = 0; dimId < _inputDimCount; ++dimId)
+ {
+ // clamp
+ if (a[dimId] > _minmax[dimId].max)
+ a[dimId] = _minmax[dimId].max;
+ else if (a[dimId] < _minmax[dimId].min)
+ a[dimId] = _minmax[dimId].min;
+
+ a[dimId] = (a[dimId] - _minmax[dimId].min) / (_minmax[dimId].max - _minmax[dimId].min);
+ }
+}
+
+} // namespace SOM
diff --git a/src/classifier/DataNormalizer.hpp b/src/classifier/DataNormalizer.hpp
new file mode 100644
index 00000000..be96dd36
--- /dev/null
+++ b/src/classifier/DataNormalizer.hpp
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2018 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include "SOM.hpp"
+
+namespace SOM
+{
+
+class DataNormalizer
+{
+ public:
+ DataNormalizer(std::size_t inputDimCount);
+
+ void computeNormalizationFactors(const std::vector& dataSamples);
+
+ void normalizeData(InputVector& data) const;
+
+ private:
+ std::size_t _inputDimCount;
+
+ struct minmax
+ {
+ InputVector::value_type min;
+ InputVector::value_type max;
+ };
+ std::vector _minmax; // Indexed min/max used to normalize data
+};
+
+} // namespace SOM
diff --git a/src/classifier/SOM.cpp b/src/classifier/SOM.cpp
new file mode 100644
index 00000000..d495b49e
--- /dev/null
+++ b/src/classifier/SOM.cpp
@@ -0,0 +1,292 @@
+/*
+ * Copyright (C) 2018 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include "SOM.hpp"
+
+#include
+#include
+#include
+#include
+
+namespace SOM
+{
+
+void
+checkSameDimensions(const InputVector& a, const InputVector& b)
+{
+ if (a.size() != b.size())
+ throw SOMException("Bad data dimension count");
+}
+
+void
+checkSameDimensions(const InputVector& a, std::size_t inputDimCount)
+{
+ if (a.size() != inputDimCount)
+ throw SOMException("Bad data dimension count");
+}
+
+InputVector::value_type
+defaultLearningFactor(Network::Progress progress)
+{
+ constexpr InputVector::value_type initialValue = 1;
+
+ return initialValue * exp(-((progress.idIteration + 1) / static_cast(progress.iterationCount)));
+}
+
+InputVector::value_type
+euclidianSquareDistance(const InputVector& a, const InputVector& b)
+{
+ InputVector::value_type res = 0;
+
+ for (std::size_t i = 0; i < a.size(); ++i)
+ {
+ res += (a[i] - b[i]) * (a[i] - b[i]);
+ }
+
+ return res;
+}
+
+static
+InputVector::value_type
+sigmaFunc(Network::Progress progress)
+{
+ constexpr InputVector::value_type sigma0 = 1;
+
+ return sigma0 * exp(- ((progress.idIteration + 1) / static_cast(progress.iterationCount)));
+}
+
+InputVector::value_type
+defaultNeighborhoodFunc(InputVector::value_type norm, Network::Progress progress)
+{
+ auto sigma = sigmaFunc(progress);
+
+ return exp(-norm / (2 * sigma * sigma));
+}
+
+
+std::ostream&
+operator<<(std::ostream& os, const InputVector& a)
+{
+ os << "[";
+ for (const auto& val : a)
+ {
+ os << val << " ";
+ }
+ os << "]";
+
+ return os;
+}
+
+
+//static
+InputVector::value_type
+norm(const InputVector& a)
+{
+ InputVector::value_type res = 0;
+
+ for (const auto& val : a)
+ {
+ res += val * val;
+ }
+
+ return sqrt(res);
+}
+
+//static
+InputVector
+operator+(const InputVector& a, const InputVector& b)
+{
+ checkSameDimensions(a, b);
+
+ InputVector res(a.size(), 0);
+
+ for (std::size_t dimId = 0; dimId < a.size(); ++dimId)
+ {
+ res[dimId] = a[dimId] + b[dimId];
+ }
+
+ return res;
+}
+
+static
+InputVector
+operator-(const InputVector& a, const InputVector& b)
+{
+ checkSameDimensions(a, b);
+
+ InputVector res(a.size(), 0);
+
+ for (std::size_t dimId = 0; dimId < a.size(); ++dimId)
+ {
+ res[dimId] = a[dimId] - b[dimId];
+ }
+
+ return res;
+}
+
+//static
+InputVector
+operator*(const InputVector& a, InputVector::value_type factor)
+{
+ InputVector res(a.size(), 0);
+
+ for (std::size_t dimId = 0; dimId < a.size(); ++dimId)
+ {
+ res[dimId] = a[dimId] * factor;
+ }
+
+ return res;
+}
+
+
+Network::Network(std::size_t width, std::size_t height, std::size_t inputDimCount)
+: _width(width),
+_height(height),
+_inputDimCount(inputDimCount),
+_distanceFunc(euclidianSquareDistance),
+_learningFactorFunc(defaultLearningFactor),
+_neighborhoodFunc(defaultNeighborhoodFunc)
+{
+ _refVectors.resize(width * height);
+
+ auto now = std::chrono::system_clock::now();
+ std::mt19937 randGenerator(std::chrono::duration_cast(now.time_since_epoch()).count());
+
+ // init each vector with a random normalized value
+ std::uniform_real_distribution dist(0, 1);
+
+ for (auto& refVector : _refVectors)
+ {
+ refVector.resize(inputDimCount);
+
+ for (auto& val : refVector)
+ val = dist(randGenerator);
+ }
+}
+
+
+InputVector&
+Network::getRefVector(std::size_t x, std::size_t y)
+{
+ return _refVectors[x + y*_width];
+}
+
+const InputVector&
+Network::getRefVector(std::size_t x, std::size_t y) const
+{
+ return _refVectors[x + y*_width];
+}
+
+
+void
+Network::dump(std::ostream& os) const
+{
+ os << "Width: " << _width << ", Height: " << _height << std::endl;;
+
+ for (std::size_t y = 0; y < _height; ++y)
+ {
+ for (std::size_t x = 0; x < _width; ++x)
+ {
+ os << getRefVector(x, y) << " ";
+ }
+
+ os << std::endl;
+ }
+ os << std::endl;
+}
+
+Coords
+Network::getClosestRefVector(const InputVector& data) const
+{
+ auto it = std::min_element(_refVectors.begin(), _refVectors.end(),
+ [&](const auto& a, const auto& b)
+ {
+ return (_distanceFunc(a, data) < _distanceFunc(b, data));
+ });
+
+ auto index = std::distance(_refVectors.begin(), it);
+
+ return {index % _height, index / _height};
+}
+
+Coords
+Network::classify(const InputVector& data) const
+{
+ return getClosestRefVector(data);
+}
+
+static InputVector::value_type
+computeCoordsNorm(Coords c1, Coords c2)
+{
+ std::vector a = { static_cast(c1.x), static_cast(c1.y) };
+ std::vector b = { static_cast(c2.x), static_cast(c2.y) };
+
+ return norm(a - b);
+}
+
+
+void
+Network::updateRefVectors(Coords closestRefVectorCoords, const InputVector& input, Progress progress)
+{
+ for (std::size_t y = 0; y < _height; ++y)
+ {
+ for (std::size_t x = 0; x < _width; ++x)
+ {
+ auto& refVector = getRefVector(x, y);
+
+ auto delta = input - refVector;
+ auto n = computeCoordsNorm({x, y}, closestRefVectorCoords);
+
+ auto oldRefVector = refVector;
+ refVector = refVector + delta * (_learningFactorFunc(progress) * _neighborhoodFunc(n, progress));
+ }
+ }
+}
+
+void
+Network::train(const std::vector& inputData, std::size_t nbIterations)
+{
+
+ std::vector inputDataShuffled;
+ inputDataShuffled.reserve(inputData.size());
+
+ for (const auto& input : inputData)
+ {
+ inputDataShuffled.push_back(&input);
+ }
+
+ std::random_device randomDevice;
+ std::mt19937 generator(randomDevice());
+
+ for (std::size_t i = 0; i < nbIterations; ++i)
+ {
+ std::shuffle(inputDataShuffled.begin(), inputDataShuffled.end(), generator);
+
+ for (auto input : inputDataShuffled)
+ {
+ Coords closestRefVectorCoords = getClosestRefVector(*input);
+
+ updateRefVectors(closestRefVectorCoords, *input, {i, nbIterations});
+ }
+ }
+}
+
+} // namespace SOM
+
+
diff --git a/src/classifier/SOM.hpp b/src/classifier/SOM.hpp
new file mode 100644
index 00000000..f9346f93
--- /dev/null
+++ b/src/classifier/SOM.hpp
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2018 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+#include
+
+#include "utils/Exception.hpp"
+
+namespace SOM
+{
+
+using InputVector = std::vector;
+void checkSameDimensions(const InputVector& a, const InputVector& b);
+void checkSameDimensions(const InputVector& a, std::size_t inputDimCount);
+std::ostream& operator<<(std::ostream& os, const InputVector& a);
+
+class SOMException : public LmsException
+{
+ public:
+ SOMException(const std::string& msg) : LmsException(msg) {}
+};
+
+// Top Left is (0,0)
+struct Coords
+{
+ std::size_t x;
+ std::size_t y;
+};
+
+class Network
+{
+ public:
+
+ Network(std::size_t width, std::size_t height, std::size_t inputDimCount);
+
+ // data must be normalized
+ void train(const std::vector& dataSamples, std::size_t nbIterations);
+
+ // data must be normalized
+ Coords classify(const InputVector& data) const;
+
+ void dump(std::ostream& os) const;
+
+ // For each ref vector, update formula is:
+ // i is the current iteration
+ // refVector(i+1) = refVector(i) + LearningFactor(i) * NeighborhoodFunc(i) * (MatchingRefVector - refVector)
+
+ using DistanceFunc = std::function;
+ void setDistanceFunc(DistanceFunc distanceFunc);
+
+ struct Progress
+ {
+ std::size_t idIteration;
+ std::size_t iterationCount;
+ };
+
+ using LearningFactorFunc = std::function;
+ void setLearningFactorFunc(LearningFactorFunc learningFactorFunc);
+
+ using NeighborhoodFunc = std::function;
+ void setNeighborhoodFunc(NeighborhoodFunc neighborhoodFunc);
+
+ private:
+
+ InputVector& getRefVector(std::size_t x, std::size_t y);
+ const InputVector& getRefVector(std::size_t x, std::size_t y) const;
+ Coords getClosestRefVector(const InputVector& data) const;
+
+ void updateRefVectors(Coords closestRefVectorCoords, const InputVector& input, Progress progress);
+
+ std::size_t _width;
+ std::size_t _height;
+ std::size_t _inputDimCount;
+
+ std::vector _refVectors; // indexed reference vectors
+
+ DistanceFunc _distanceFunc;
+ LearningFactorFunc _learningFactorFunc;
+ NeighborhoodFunc _neighborhoodFunc;
+};
+
+} // namespace SOM
diff --git a/tools/Makefile.am b/tools/Makefile.am
index 085ca08c..16922b29 100644
--- a/tools/Makefile.am
+++ b/tools/Makefile.am
@@ -1,2 +1,2 @@
-SUBDIRS = metadata
+SUBDIRS = metadata classifier
diff --git a/tools/classifier/LmsClassifier.cpp b/tools/classifier/LmsClassifier.cpp
new file mode 100644
index 00000000..36568e42
--- /dev/null
+++ b/tools/classifier/LmsClassifier.cpp
@@ -0,0 +1,80 @@
+#include
+
+#include
+#include
+#include
+
+#include "classifier/SOM.hpp"
+#include "classifier/DataNormalizer.hpp"
+
+int main(int argc, char *argv[])
+{
+ if (argc != 2)
+ {
+ std::cerr << "Usage: " << std::endl;
+ return EXIT_FAILURE;
+ }
+
+ auto iterationCount = std::stoul(argv[1]);
+
+ using namespace SOM;
+
+ SOM::Network network(5, 5, 2);
+
+ network.dump(std::cout);
+
+ std::vector< std::vector > inputValues =
+ {
+ { 160, 1 },
+ { 80, -1 },
+ { 80, -0.75 },
+ { 240, 0.5 },
+ { 120, -0.5 },
+ { 140, -0.5 },
+ };
+
+ std::cout << "Before normalization:" << std::endl;
+ for (const auto& inputValue : inputValues)
+ {
+ std::cout << inputValue << std::endl;
+ }
+ std::cout << std::endl;
+
+ std::cout << "After normalization:" << std::endl;
+
+ SOM::DataNormalizer normalizer(2);
+
+ normalizer.computeNormalizationFactors(inputValues);
+
+ for (auto& inputValue : inputValues)
+ normalizer.normalizeData(inputValue);
+
+ for (const auto& inputValue : inputValues)
+ {
+ std::cout << inputValue << std::endl;
+ }
+
+ std::cout << std::endl;
+
+ for (const auto& inputValue : inputValues)
+ {
+ auto res = network.classify(inputValue);
+ std::cout << "Found at " << res.x << ", " << res.y << std::endl;
+ }
+
+ std::cout << "Training network for " << iterationCount << " iterations" << std::endl;
+
+ network.train(inputValues, iterationCount);
+
+ network.dump(std::cout);
+ std::cout << "OK" << std::endl;
+
+ for (const auto& inputValue : inputValues)
+ {
+ auto res = network.classify(inputValue);
+ std::cout << "Found at " << res.x << ", " << res.y << std::endl;
+ }
+
+ return EXIT_SUCCESS;
+}
+
diff --git a/tools/classifier/Makefile.am b/tools/classifier/Makefile.am
new file mode 100644
index 00000000..2d036d25
--- /dev/null
+++ b/tools/classifier/Makefile.am
@@ -0,0 +1,11 @@
+bin_PROGRAMS = lms-classifier
+
+lms_classifier_SOURCES = \
+ $(srcdir)/LmsClassifier.cpp \
+ $(top_srcdir)/src/classifier/DataNormalizer.cpp \
+ $(top_srcdir)/src/classifier/SOM.cpp \
+ $(top_srcdir)/src/utils/Logger.cpp \
+ $(top_srcdir)/src/utils/Utils.cpp
+
+lms_classifier_CXXFLAGS=-std=c++14 -Wall -I$(top_srcdir)/src -D_REENTRANT
+