From 4a06a5a1cfb01ee796f428a305583180ffb21ae7 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 7 Dec 2018 13:52:41 +0100 Subject: [PATCH] WIP som network --- configure.ac | 3 +- src/classifier/DataNormalizer.cpp | 74 ++++++++ src/classifier/DataNormalizer.hpp | 47 +++++ src/classifier/SOM.cpp | 292 +++++++++++++++++++++++++++++ src/classifier/SOM.hpp | 101 ++++++++++ tools/Makefile.am | 2 +- tools/classifier/LmsClassifier.cpp | 80 ++++++++ tools/classifier/Makefile.am | 11 ++ 8 files changed, 608 insertions(+), 2 deletions(-) create mode 100644 src/classifier/DataNormalizer.cpp create mode 100644 src/classifier/DataNormalizer.hpp create mode 100644 src/classifier/SOM.cpp create mode 100644 src/classifier/SOM.hpp create mode 100644 tools/classifier/LmsClassifier.cpp create mode 100644 tools/classifier/Makefile.am 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 +