From 45b4468c01167f52a5608cbba13a66230d41af4b Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 8 Mar 2019 15:46:29 +0100 Subject: [PATCH] Refactored the inputvector part of the som implementation --- configure.ac | 2 +- src/Makefile.am | 1 + src/database/Track.cpp | 8 +- src/database/Track.hpp | 2 +- .../features/SimilarityFeaturesCache.cpp | 254 +++++++++++ .../features/SimilarityFeaturesCache.hpp | 50 +++ .../SimilarityFeaturesScannerAddon.cpp | 51 ++- .../features/SimilarityFeaturesSearcher.cpp | 411 +++++------------- .../features/SimilarityFeaturesSearcher.hpp | 38 +- .../features/som/DataNormalizer.cpp | 20 +- .../features/som/DataNormalizer.hpp | 2 +- src/similarity/features/som/InputVector.hpp | 194 +++++++++ src/similarity/features/som/Matrix.hpp | 25 +- src/similarity/features/som/Network.cpp | 221 +++------- src/similarity/features/som/Network.hpp | 44 +- test/Makefile.am | 11 +- tools/Makefile.am | 2 +- .../LmsSimilarity.cpp} | 95 ++-- .../Makefile.am | 8 +- 19 files changed, 839 insertions(+), 600 deletions(-) create mode 100644 src/similarity/features/SimilarityFeaturesCache.cpp create mode 100644 src/similarity/features/SimilarityFeaturesCache.hpp create mode 100644 src/similarity/features/som/InputVector.hpp rename tools/{feature-extractor/LmsFeatureExtractor.cpp => similarity/LmsSimilarity.cpp} (72%) rename tools/{feature-extractor => similarity}/Makefile.am (78%) diff --git a/configure.ac b/configure.ac index 9bf71e41..3af2f9ec 100644 --- a/configure.ac +++ b/configure.ac @@ -85,7 +85,7 @@ AC_CONFIG_FILES([Makefile src/Makefile test/Makefile tools/Makefile - tools/feature-extractor/Makefile + tools/similarity/Makefile tools/metadata/Makefile]) AC_OUTPUT diff --git a/src/Makefile.am b/src/Makefile.am index 197bec99..6eb370e4 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -24,6 +24,7 @@ lms_SOURCES = \ $(srcdir)/similarity/SimilaritySearcher.cpp \ $(srcdir)/similarity/cluster/SimilarityClusterSearcher.cpp \ $(srcdir)/similarity/features/AcousticBrainzUtils.cpp \ + $(srcdir)/similarity/features/SimilarityFeaturesCache.cpp \ $(srcdir)/similarity/features/SimilarityFeaturesScannerAddon.cpp \ $(srcdir)/similarity/features/SimilarityFeaturesSearcher.cpp \ $(srcdir)/similarity/features/som/DataNormalizer.cpp \ diff --git a/src/database/Track.cpp b/src/database/Track.cpp index df44a28a..5e60049a 100644 --- a/src/database/Track.cpp +++ b/src/database/Track.cpp @@ -119,11 +119,15 @@ Track::getAllWithMBIDAndMissingFeatures(Wt::Dbo::Session& session) } std::vector -Track::getAllWithFeatures(Wt::Dbo::Session& session) +Track::getAllWithFeatures(Wt::Dbo::Session& session, boost::optional limit) { + int size {limit ? static_cast(*limit) : -1}; + Wt::Dbo::collection res = session.query ("SELECT t FROM track t") - .where("EXISTS (SELECT * from track_features t_f WHERE t_f.track_id = t.id)"); + .where("EXISTS (SELECT * from track_features t_f WHERE t_f.track_id = t.id)") + .limit(size); + return std::vector(res.begin(), res.end()); } diff --git a/src/database/Track.hpp b/src/database/Track.hpp index 9ad21ddc..663ef9cc 100644 --- a/src/database/Track.hpp +++ b/src/database/Track.hpp @@ -70,7 +70,7 @@ class Track : public Wt::Dbo::Dbo static std::vector getChecksumDuplicates(Wt::Dbo::Session& session); static std::vector getLastAdded(Wt::Dbo::Session& session, Wt::WDateTime after, int size = 1); static std::vector getAllWithMBIDAndMissingFeatures(Wt::Dbo::Session& session); // nested transaction - static std::vector getAllWithFeatures(Wt::Dbo::Session& session); // nested transaction + static std::vector getAllWithFeatures(Wt::Dbo::Session& session, boost::optional limit = {}); // nested transaction // Create utility static pointer create(Wt::Dbo::Session& session, const boost::filesystem::path& p); diff --git a/src/similarity/features/SimilarityFeaturesCache.cpp b/src/similarity/features/SimilarityFeaturesCache.cpp new file mode 100644 index 00000000..8bfea439 --- /dev/null +++ b/src/similarity/features/SimilarityFeaturesCache.cpp @@ -0,0 +1,254 @@ +/* + * 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 "SimilarityFeaturesCache.hpp" + +#include +#include + +#include "utils/Config.hpp" +#include "utils/Logger.hpp" +#include "utils/Utils.hpp" + +namespace Similarity { + + +static +boost::filesystem::path getCacheDirectory() +{ + return Config::instance().getPath("working-dir") / "cache" / "features"; +} + +static boost::filesystem::path getCacheNetworkFilePath() +{ + return getCacheDirectory() / "network"; +}; + +static boost::filesystem::path getCacheTrackPositionsFilePath() +{ + return getCacheDirectory() / "track_positions"; +} + +static +bool +networkToCacheFile(const SOM::Network& network, boost::filesystem::path path) +{ + try + { + boost::property_tree::ptree root; + + root.put("width", network.getWidth()); + root.put("height", network.getHeight()); + root.put("dim_count", network.getInputDimCount()); + + for (SOM::InputVector::value_type weight : network.getDataWeights()) + root.add("weights.weight", weight); + + for (SOM::Coordinate x = 0; x < network.getWidth(); ++x) + { + for (SOM::Coordinate y = 0; y < network.getWidth(); ++y) + { + const auto& refVector = network.getRefVector({x, y}); + + boost::property_tree::ptree node; + for (auto value : refVector) + node.add("values.value", value); + + node.put("coord_x", x); + node.put("coord_y", y); + + root.add_child("ref_vectors.ref_vector", node); + } + } + + boost::property_tree::write_xml(path.string(), root); + + LMS_LOG(SIMILARITY, DEBUG) << "Created network cache"; + return true; + } + catch (boost::property_tree::ptree_error& error) + { + LMS_LOG(SIMILARITY, ERROR) << "Cannot create network cache: " << error.what(); + return false; + } +} + +static +boost::optional +createNetworkFromCacheFile(boost::filesystem::path path) +{ + try + { + boost::property_tree::ptree root; + + boost::property_tree::read_xml(path.string(), root); + + SOM::Coordinate width {root.get("width")}; + SOM::Coordinate height {root.get("height")}; + std::size_t dimCount {root.get("dim_count")}; + + SOM::Network res {width, height, dimCount}; + + { + SOM::InputVector weights {dimCount}; + std::size_t i {}; + for (const auto& val : root.get_child("weights")) + weights[i++] = val.second.get_value(); + + res.setDataWeights(weights); + } + + for (const auto& node : root.get_child("ref_vectors")) + { + SOM::Coordinate x {node.second.get("coord_x")}; + SOM::Coordinate y {node.second.get("coord_y")}; + + SOM::InputVector refVector {dimCount}; + std::size_t i {}; + for (const auto& val : node.second.get_child("values")) + refVector[i++] = val.second.get_value(); + + res.setRefVector({x, y}, refVector); + } + + LMS_LOG(SIMILARITY, DEBUG) << "Successfully read network from cache"; + + return res; + } + catch (boost::property_tree::ptree_error& error) + { + LMS_LOG(SIMILARITY, ERROR) << "Cannot read network cache: " << error.what(); + return boost::none; + } +} + +static +bool +objectPositionToCacheFile(const std::map>& objectsPosition, boost::filesystem::path path) +{ + try + { + boost::property_tree::ptree root; + + for (const auto& objectPosition : objectsPosition) + { + boost::property_tree::ptree node; + + node.put("id", objectPosition.first); + + for (const auto& position : objectPosition.second) + { + boost::property_tree::ptree positionNode; + positionNode.put("x", position.x); + positionNode.put("y", position.y); + + node.add_child("position.position", positionNode); + } + + root.add_child("objects.object", node); + } + + boost::property_tree::write_xml(path.string(), root); + return true; + } + catch (boost::property_tree::ptree_error& error) + { + LMS_LOG(SIMILARITY, ERROR) << "Cannot cache object position: " << error.what(); + return false; + } +} + +static +boost::optional>> +createObjectPositionsFromCacheFile(boost::filesystem::path path) +{ + try + { + boost::property_tree::ptree root; + + boost::property_tree::read_xml(path.string(), root); + + std::map> res; + + for (const auto& object : root.get_child("objects")) + { + auto id = object.second.get("id"); + for (const auto& position : object.second.get_child("position")) + { + auto x = position.second.get("x"); + auto y = position.second.get("y"); + + res[id].insert({x, y}); + } + } + + LMS_LOG(SIMILARITY, DEBUG) << "Successfully read object position from cache"; + + return res; + } + catch (boost::property_tree::ptree_error& error) + { + LMS_LOG(SIMILARITY, ERROR) << "Cannot create object position from cache file: " << error.what(); + return boost::none; + } +} + +void +FeaturesCache::invalidate() +{ + boost::filesystem::remove(getCacheNetworkFilePath()); + boost::filesystem::remove(getCacheTrackPositionsFilePath()); +} + +boost::optional +FeaturesCache::read() +{ + boost::optional res; + + auto network{createNetworkFromCacheFile(getCacheNetworkFilePath())}; + if (!network) + return res; + + auto trackPositions{createObjectPositionsFromCacheFile(getCacheTrackPositionsFilePath())}; + if (!trackPositions) + return res; + + return FeaturesCache{std::move(*network), std::move(*trackPositions)}; +} + +void +FeaturesCache::write() +{ + boost::filesystem::create_directories(Config::instance().getPath("working-dir") / "cache" / "features"); + + if (!networkToCacheFile(_network, getCacheNetworkFilePath()) + || !objectPositionToCacheFile(_trackPositions, getCacheTrackPositionsFilePath())) + { + invalidate(); + } +} + +FeaturesCache::FeaturesCache(SOM::Network network, ObjectPositions trackPositions) +: _network{std::move(network)}, +_trackPositions{std::move(trackPositions)} +{ +} + + +} // namespace Similarity diff --git a/src/similarity/features/SimilarityFeaturesCache.hpp b/src/similarity/features/SimilarityFeaturesCache.hpp new file mode 100644 index 00000000..f2ce9c9a --- /dev/null +++ b/src/similarity/features/SimilarityFeaturesCache.hpp @@ -0,0 +1,50 @@ +/* + * 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 "database/Types.hpp" +#include "som/Network.hpp" + +namespace Similarity { + +class FeaturesCache +{ + public: + + static void invalidate(); + + static boost::optional read(); + void write(); + + private: + using ObjectPositions = std::map>; + + FeaturesCache(SOM::Network network, ObjectPositions trackPositions); + + friend class FeaturesSearcher; + + SOM::Network _network; + ObjectPositions _trackPositions; +}; + +} // namespace Similarity diff --git a/src/similarity/features/SimilarityFeaturesScannerAddon.cpp b/src/similarity/features/SimilarityFeaturesScannerAddon.cpp index ad2b303c..be65f807 100644 --- a/src/similarity/features/SimilarityFeaturesScannerAddon.cpp +++ b/src/similarity/features/SimilarityFeaturesScannerAddon.cpp @@ -19,15 +19,13 @@ #include "SimilarityFeaturesScannerAddon.hpp" - +#include "AcousticBrainzUtils.hpp" #include "database/Track.hpp" #include "database/TrackFeatures.hpp" +#include "similarity/features/SimilarityFeaturesCache.hpp" #include "utils/Config.hpp" #include "utils/Logger.hpp" -#include "AcousticBrainzUtils.hpp" - - namespace Similarity { namespace { @@ -45,8 +43,8 @@ getTracksWithMBIDAndMissingFeatures(Wt::Dbo::Session& session) Wt::Dbo::Transaction transaction(session); - auto tracks = Database::Track::getAllWithMBIDAndMissingFeatures(session); - for (auto track : tracks) + auto tracks {Database::Track::getAllWithMBIDAndMissingFeatures(session)}; + for (const Database::Track::pointer& track : tracks) res.push_back({track.id(), track->getMBID()}); return res; @@ -57,11 +55,13 @@ getTracksWithMBIDAndMissingFeatures(Wt::Dbo::Session& session) FeaturesScannerAddon::FeaturesScannerAddon(Wt::Dbo::SqlConnectionPool& connectionPool) : _db(connectionPool) { - boost::filesystem::create_directories(Config::instance().getPath("working-dir") / "cache" / "features"); - - auto searcher = std::make_shared(); - if (searcher->initFromCache(_db.getSession())) - std::atomic_store(&_searcher, searcher); + boost::optional cache {Similarity::FeaturesCache::read()}; + if (cache) + { + auto searcher {std::make_shared(_db.getSession(), *cache)}; + if (searcher->isValid()) + std::atomic_store(&_searcher, searcher); + } } std::shared_ptr @@ -92,13 +92,13 @@ void FeaturesScannerAddon::preScanComplete() { LMS_LOG(DBUPDATER, DEBUG) << "Getting tracks with missing Features..."; - auto tracksInfo = getTracksWithMBIDAndMissingFeatures(_db.getSession()); + std::vector tracksInfo {getTracksWithMBIDAndMissingFeatures(_db.getSession())}; LMS_LOG(DBUPDATER, DEBUG) << "Getting tracks with missing Features DONE (found " << tracksInfo.size() << ")"; - for (const auto& trackInfo : tracksInfo) + for (const TrackInfo& trackInfo : tracksInfo) fetchFeatures(trackInfo.id, trackInfo.mbid); - FeaturesSearcher::invalidateCache(); + Similarity::FeaturesCache::invalidate(); updateSearcher(); } @@ -112,15 +112,24 @@ FeaturesScannerAddon::updateSearcher() if (tracks.empty()) { LMS_LOG(DBUPDATER, INFO) << "No track suitable for features similarity clustering"; - std::atomic_store(&_searcher, std::shared_ptr()); + std::atomic_store(&_searcher, std::shared_ptr{}); return; } - auto searcher {std::make_shared()}; - if (searcher->init(_db.getSession(), _stopRequested)) + auto searcher {std::make_shared(_db.getSession(), _stopRequested)}; + if (searcher->isValid()) + { std::atomic_store(&_searcher, searcher); + FeaturesCache cache{searcher->toCache()}; + cache.write(); + + searcher->dump(_db.getSession(), std::cout); + + LMS_LOG(DBUPDATER, INFO) << "New features similarity searcher instanciated"; + } + else + std::atomic_store(&_searcher, std::shared_ptr{}); - LMS_LOG(DBUPDATER, INFO) << "New features similarity searcher instanciated"; } bool @@ -129,7 +138,7 @@ FeaturesScannerAddon::fetchFeatures(Database::IdType trackId, const std::string& std::map features; LMS_LOG(DBUPDATER, DEBUG) << "Fetching low level features for track '" << MBID << "'"; - std::string data = AcousticBrainz::extractLowLevelFeatures(MBID); + std::string data {AcousticBrainz::extractLowLevelFeatures(MBID)}; if (data.empty()) { @@ -139,9 +148,9 @@ FeaturesScannerAddon::fetchFeatures(Database::IdType trackId, const std::string& // TODO check if the expected features are here - Wt::Dbo::Transaction transaction(_db.getSession()); + Wt::Dbo::Transaction transaction{_db.getSession()}; - Wt::Dbo::ptr track = Database::Track::getById(_db.getSession(), trackId); + Wt::Dbo::ptr track {Database::Track::getById(_db.getSession(), trackId)}; if (!track) return false; diff --git a/src/similarity/features/SimilarityFeaturesSearcher.cpp b/src/similarity/features/SimilarityFeaturesSearcher.cpp index 56973fa0..1ca710f5 100644 --- a/src/similarity/features/SimilarityFeaturesSearcher.cpp +++ b/src/similarity/features/SimilarityFeaturesSearcher.cpp @@ -20,8 +20,6 @@ #include "SimilarityFeaturesSearcher.hpp" #include -#include -#include #include "database/Artist.hpp" #include "database/SimilaritySettings.hpp" @@ -29,237 +27,82 @@ #include "database/Track.hpp" #include "database/TrackFeatures.hpp" #include "som/DataNormalizer.hpp" -#include "utils/Config.hpp" #include "utils/Logger.hpp" #include "utils/Utils.hpp" namespace Similarity { -static -boost::filesystem::path getCacheDirectory() +struct FeatureInfo { - return Config::instance().getPath("working-dir") / "cache" / "features"; -} - -static boost::filesystem::path getCacheNetworkFilePath() -{ - return getCacheDirectory() / "network"; + std::size_t nbDimensions; + double weight; }; -static boost::filesystem::path getCacheTrackPositionsFilePath() +using FeatureInfoMap = std::map; + +static +FeatureInfoMap +getFeatureInfoMap(Wt::Dbo::Session& session) { - return getCacheDirectory() / "track_positions"; + Wt::Dbo::Transaction transaction {session}; + + auto settings {Database::SimilaritySettings::get(session)}; + + std::map featuresInfo; + for (auto feature : settings->getFeatures()) + { + LMS_LOG(SIMILARITY, DEBUG) << "Feature '" << feature->getName() << "', nbDimns = " << feature->getNbDimensions() << ", weight = " << feature->getWeight() ; + featuresInfo[feature->getName()] = { feature->getNbDimensions(), feature->getWeight() }; + } + + return featuresInfo; } static -bool -networkToCacheFile(const SOM::Network& network, boost::filesystem::path path) +std::size_t +getFeatureInfoMapNbDimensions(const FeatureInfoMap& featureInfoMap) { - try - { - boost::property_tree::ptree root; - - root.put("width", network.getWidth()); - root.put("height", network.getHeight()); - root.put("dim_count", network.getInputDimCount()); - - for (auto weight : network.getDataWeights()) - root.add("weights.weight", weight); - - for (SOM::Coordinate x = 0; x < network.getWidth(); ++x) - { - for (SOM::Coordinate y = 0; y < network.getWidth(); ++y) - { - const auto& refVector = network.getRefVector({x, y}); - - boost::property_tree::ptree node; - for (auto value : refVector) - node.add("values.value", value); - - node.put("coord_x", x); - node.put("coord_y", y); - - root.add_child("ref_vectors.ref_vector", node); - } - } - - boost::property_tree::write_xml(path.string(), root); - - LMS_LOG(SIMILARITY, DEBUG) << "Created network cache"; - return true; - } - catch (boost::property_tree::ptree_error& error) - { - LMS_LOG(SIMILARITY, ERROR) << "Cannot create network cache: " << error.what(); - return false; - } + return std::accumulate(featureInfoMap.begin(), featureInfoMap.end(), 0, [](std::size_t sum, auto it) { return sum + it.second.nbDimensions; }); } -static -boost::optional -createNetworkFromCacheFile(boost::filesystem::path path) -{ - try - { - boost::property_tree::ptree root; - - boost::property_tree::read_xml(path.string(), root); - - auto width = root.get("width"); - auto height = root.get("height"); - auto dimCount = root.get("dim_count"); - - SOM::Network res(width, height, dimCount); - - SOM::InputVector weights; - for (const auto& val : root.get_child("weights")) - weights.push_back(val.second.get_value()); - - res.setDataWeights(weights); - - for (const auto& node : root.get_child("ref_vectors")) - { - auto x = node.second.get("coord_x"); - auto y = node.second.get("coord_y"); - - std::vector values; - for (const auto& val : node.second.get_child("values")) - values.push_back(val.second.get_value()); - - res.setRefVector({x, y}, values); - } - - LMS_LOG(SIMILARITY, DEBUG) << "Successfully read network from cache"; - - return res; - } - catch (boost::property_tree::ptree_error& error) - { - LMS_LOG(SIMILARITY, ERROR) << "Cannot read network cache: " << error.what(); - return boost::none; - } -} - -static -bool -objectPositionToCacheFile(const std::map>& objectsPosition, boost::filesystem::path path) -{ - try - { - boost::property_tree::ptree root; - - for (const auto& objectPosition : objectsPosition) - { - boost::property_tree::ptree node; - - node.put("id", objectPosition.first); - - for (const auto& position : objectPosition.second) - { - boost::property_tree::ptree positionNode; - positionNode.put("x", position.x); - positionNode.put("y", position.y); - - node.add_child("position.position", positionNode); - } - - root.add_child("objects.object", node); - } - - boost::property_tree::write_xml(path.string(), root); - return true; - } - catch (boost::property_tree::ptree_error& error) - { - LMS_LOG(SIMILARITY, ERROR) << "Cannot cache object position: " << error.what(); - return false; - } -} - -static -boost::optional>> -createObjectPositionsFromCacheFile(boost::filesystem::path path) -{ - try - { - boost::property_tree::ptree root; - - boost::property_tree::read_xml(path.string(), root); - - std::map> res; - - for (const auto& object : root.get_child("objects")) - { - auto id = object.second.get("id"); - for (const auto& position : object.second.get_child("position")) - { - auto x = position.second.get("x"); - auto y = position.second.get("y"); - - res[id].insert({x, y}); - } - } - - LMS_LOG(SIMILARITY, DEBUG) << "Successfully read object position from cache"; - - return res; - } - catch (boost::property_tree::ptree_error& error) - { - LMS_LOG(SIMILARITY, ERROR) << "Cannot create object position from cache file: " << error.what(); - return boost::none; - } -} - -bool -FeaturesSearcher::init(Wt::Dbo::Session& session, bool& stopRequested) +FeaturesSearcher::FeaturesSearcher(Wt::Dbo::Session& session, bool& stopRequested) { Wt::Dbo::Transaction transaction(session); - auto settings = Database::SimilaritySettings::get(session); + FeatureInfoMap featuresInfo {getFeatureInfoMap(session)}; + std::size_t nbDimensions {getFeatureInfoMapNbDimensions(featuresInfo)}; - struct FeatureInfo - { - std::size_t nbDimensions; - double weight; - }; - - std::map featuresInfo; - std::size_t nbDimensions = 0; - for (auto feature : settings->getFeatures()) - { - featuresInfo[feature->getName()] = { feature->getNbDimensions(), feature->getWeight() }; - nbDimensions += feature->getNbDimensions(); - } + LMS_LOG(SIMILARITY, DEBUG) << "Features dimension = " << nbDimensions; LMS_LOG(SIMILARITY, DEBUG) << "Getting Tracks with features..."; - auto tracks = Database::Track::getAllWithFeatures(session); + auto tracks {Database::Track::getAllWithFeatures(session)}; LMS_LOG(SIMILARITY, DEBUG) << "Getting Tracks with features DONE"; std::vector samples; std::vector tracksIds; LMS_LOG(SIMILARITY, DEBUG) << "Extracting features..."; - for (auto track : tracks) + for (const Database::Track::pointer& track : tracks) { if (stopRequested) - return false; + return; - SOM::InputVector sample; + SOM::InputVector sample {nbDimensions}; std::map> features; - for (const auto& featureInfo : featuresInfo) - features[featureInfo.first] = {}; + for (auto itFeatureInfo : featuresInfo) + features[itFeatureInfo.first] = {}; if (!track->getTrackFeatures()->getFeatures(features)) continue; - // Check dimensions for each feature - bool ok = true; + bool ok {true}; + std::size_t i {}; for (const auto& feature : features) { - auto it = featuresInfo.find(feature.first); + // Check dimensions for each feature + auto it {featuresInfo.find(feature.first)}; if (it == featuresInfo.end() || it->second.nbDimensions != feature.second.size()) { LMS_LOG(SIMILARITY, WARNING) << "Dimension mismatch for feature '" << feature.first << "'. Expected " << it->second.nbDimensions << ", got " << feature.second.size(); @@ -267,7 +110,8 @@ FeaturesSearcher::init(Wt::Dbo::Session& session, bool& stopRequested) break; } - sample.insert( sample.end(), feature.second.begin(), feature.second.end() ); + for (double val : feature.second) + sample[i++] = val; } if (!ok) @@ -283,7 +127,7 @@ FeaturesSearcher::init(Wt::Dbo::Session& session, bool& stopRequested) if (tracksIds.empty()) { LMS_LOG(SIMILARITY, INFO) << "Nothing to classify!"; - return false; + return; } LMS_LOG(SIMILARITY, DEBUG) << "Normalizing data..."; @@ -293,17 +137,21 @@ FeaturesSearcher::init(Wt::Dbo::Session& session, bool& stopRequested) for (auto& sample : samples) dataNormalizer.normalizeData(sample); - std::size_t size = std::sqrt(samples.size()/2); - LMS_LOG(SIMILARITY, INFO) << "Found " << samples.size() << " tracks, constructing a " << size << "*" << size << " network"; - - std::vector weights; - for (const auto& featureInfo : featuresInfo) + SOM::InputVector weights {nbDimensions}; { - for (std::size_t i = 0; i < featureInfo.second.nbDimensions; ++i) - weights.push_back(1. / featureInfo.second.nbDimensions * featureInfo.second.weight); + std::size_t index {}; + for (const auto& featureInfo : featuresInfo) + { + for (std::size_t i {}; i < featureInfo.second.nbDimensions; ++i) + weights[index++] = (1. / featureInfo.second.nbDimensions * featureInfo.second.weight); + } } - SOM::Network network(size, size, nbDimensions); + SOM::Coordinate size {static_cast(std::sqrt(samples.size() / 4))}; + LMS_LOG(SIMILARITY, INFO) << "Found " << samples.size() << " tracks, constructing a " << size << "*" << size << " network"; + + SOM::Network network {size, size, nbDimensions}; + std::cout << "Weights = '" << weights << "'"; network.setDataWeights(weights); auto progressIndicator{[](const auto& iter) @@ -314,116 +162,100 @@ FeaturesSearcher::init(Wt::Dbo::Session& session, bool& stopRequested) auto stopper{[&]() { return stopRequested; }}; LMS_LOG(SIMILARITY, DEBUG) << "Training network..."; - network.train(samples, 1, progressIndicator, stopper); + network.train(samples, 10, progressIndicator, stopper); LMS_LOG(SIMILARITY, DEBUG) << "Training network DONE"; if (stopRequested) - return false; + return; LMS_LOG(SIMILARITY, DEBUG) << "Classifying tracks..."; - std::map> trackPosition; - for (std::size_t i = 0; i < samples.size(); ++i) + std::map> trackPositions; + for (std::size_t i {}; i < samples.size(); ++i) { if (stopRequested) - return false; + return; - Wt::Dbo::Transaction transaction(session); + Wt::Dbo::Transaction transaction {session}; const auto& sample = samples[i]; auto trackId = tracksIds[i]; auto position = network.getClosestRefVectorPosition(sample); - trackPosition[trackId].insert(position); + trackPositions[trackId].insert(position); } LMS_LOG(SIMILARITY, DEBUG) << "Classifying tracks DONE"; - init(session, std::move(network), std::move(trackPosition)); + init(session, std::move(network), std::move(trackPositions)); +} - saveToCache(); +FeaturesSearcher::FeaturesSearcher(Wt::Dbo::Session& session, FeaturesCache cache) +{ + init(session, std::move(cache._network), std::move(cache._trackPositions)); - return true; + LMS_LOG(SIMILARITY, DEBUG) << "Init from cache DONE"; } bool -FeaturesSearcher::initFromCache(Wt::Dbo::Session& session) +FeaturesSearcher::isValid() const { - auto network{createNetworkFromCacheFile(getCacheNetworkFilePath())}; - if (!network) - { - clearCache(); - return false; - } - - auto trackPositions{createObjectPositionsFromCacheFile(getCacheTrackPositionsFilePath())}; - if (!trackPositions) - { - clearCache(); - return false; - } - - init(session, std::move(*network), std::move(*trackPositions)); - - LMS_LOG(SIMILARITY, DEBUG) << "Init from cache OK"; - - return true; -} - -void -FeaturesSearcher::invalidateCache() -{ - boost::filesystem::remove(getCacheNetworkFilePath()); - boost::filesystem::remove(getCacheTrackPositionsFilePath()); + return _network.get() != nullptr; } std::vector FeaturesSearcher::getSimilarTracks(const std::set& tracksIds, std::size_t maxCount) const { - return getSimilarObjects(tracksIds, _tracksMap, _trackPosition, maxCount); + return getSimilarObjects(tracksIds, _tracksMap, _trackPositions, maxCount); } std::vector FeaturesSearcher::getSimilarReleases(Database::IdType releaseId, std::size_t maxCount) const { - return getSimilarObjects({releaseId}, _releasesMap, _releasePosition, maxCount); + return getSimilarObjects({releaseId}, _releasesMap, _releasePositions, maxCount); } std::vector FeaturesSearcher::getSimilarArtists(Database::IdType artistId, std::size_t maxCount) const { - return getSimilarObjects({artistId}, _artistsMap, _artistPosition, maxCount); + return getSimilarObjects({artistId}, _artistsMap, _artistPositions, maxCount); } void FeaturesSearcher::dump(Wt::Dbo::Session& session, std::ostream& os) const { - os << "Number of tracks classified: " << _trackPosition.size() << std::endl; - os << "Network size: " << _network.getWidth() << " * " << _network.getHeight() << std::endl; + if (!isValid()) + { + os << "Invalid searcher" << std::endl; + return; + } + + os << "Number of tracks classified: " << _trackPositions.size() << std::endl; + os << "Network size: " << _network->getWidth() << " * " << _network->getHeight() << std::endl; os << "Ref vectors median distance = " << _networkRefVectorsDistanceMedian << std::endl; Wt::Dbo::Transaction transaction(session); - for (SOM::Coordinate y = 0; y < _network.getHeight(); ++y) + for (SOM::Coordinate y {}; y < _network->getHeight(); ++y) { - for (SOM::Coordinate x = 0; x < _network.getWidth(); ++x) + for (SOM::Coordinate x {}; x < _network->getWidth(); ++x) { - const auto& trackIds = _tracksMap[{x, y}]; + const auto& trackIds {_tracksMap[{x, y}]}; os << "{" << x << ", " << y << "}"; if (y > 0) - os << " - {" << x << ", " << y - 1 << "}: " << _network.getRefVectorsDistance({x, y}, {x, y - 1}); + os << " - {" << x << ", " << y - 1 << "}: " << _network->getRefVectorsDistance({x, y}, {x, y - 1}); if (x > 0) - os << " - {" << x - 1 << ", " << y << "}: " << _network.getRefVectorsDistance({x, y}, {x - 1, y}); - if (y != _network.getHeight() - 1) - os << " - {" << x << ", " << y + 1 << "}: " << _network.getRefVectorsDistance({x, y}, {x, y + 1}); - if (x != _network.getWidth() - 1) - os << " - {" << x + 1 << ", " << y << "}: " << _network.getRefVectorsDistance({x, y}, {x + 1, y}); + os << " - {" << x - 1 << ", " << y << "}: " << _network->getRefVectorsDistance({x, y}, {x - 1, y}); + if (y != _network->getHeight() - 1) + os << " - {" << x << ", " << y + 1 << "}: " << _network->getRefVectorsDistance({x, y}, {x, y + 1}); + if (x != _network->getWidth() - 1) + os << " - {" << x + 1 << ", " << y << "}: " << _network->getRefVectorsDistance({x, y}, {x + 1, y}); os << std::endl; - for (auto trackId : trackIds) + for (Database::IdType trackId : trackIds) { - auto track = Database::Track::getById(session, trackId); + auto track {Database::Track::getById(session, trackId)}; if (!track) continue; @@ -440,48 +272,52 @@ FeaturesSearcher::dump(Wt::Dbo::Session& session, std::ostream& os) const } } +FeaturesCache +FeaturesSearcher::toCache() const +{ + return FeaturesCache{*_network, _trackPositions}; +} void FeaturesSearcher::init(Wt::Dbo::Session& session, SOM::Network network, std::map> tracksPosition) { - _network = std::move(network); - - _networkRefVectorsDistanceMedian = _network.computeRefVectorsDistanceMedian(); + _network = std::make_unique(std::move(network)); + _networkRefVectorsDistanceMedian = _network->computeRefVectorsDistanceMedian(); LMS_LOG(SIMILARITY, DEBUG) << "Median distance betweend ref vectors = " << _networkRefVectorsDistanceMedian; - auto width = _network.getWidth(); - auto height = _network.getHeight(); + SOM::Coordinate width {_network->getWidth()}; + SOM::Coordinate height {_network->getHeight()}; _artistsMap = SOM::Matrix>(width, height); _releasesMap = SOM::Matrix>(width, height); _tracksMap = SOM::Matrix>(width, height); - Wt::Dbo::Transaction transaction(session); + Wt::Dbo::Transaction transaction {session}; for (auto itTrackCoord : tracksPosition) { - auto trackId = itTrackCoord.first; - const auto& positionSet = itTrackCoord.second; + Database::IdType trackId {itTrackCoord.first}; + const std::set& positionSet {itTrackCoord.second}; - auto track = Database::Track::getById(session, trackId); + auto track {Database::Track::getById(session, trackId)}; if (!track) continue; - for (const auto& position : positionSet) + for (const SOM::Position& position : positionSet) { _tracksMap[position].insert(trackId); - _trackPosition[trackId].insert(position); + _trackPositions[trackId].insert(position); if (track->getRelease()) { - _releasePosition[track->getRelease().id()].insert(position); + _releasePositions[track->getRelease().id()].insert(position); _releasesMap[position].insert(track->getRelease().id()); } if (track->getArtist()) { - _artistPosition[track->getArtist().id()].insert(position); + _artistPositions[track->getArtist().id()].insert(position); _artistsMap[position].insert(track->getArtist().id()); } } @@ -491,25 +327,6 @@ FeaturesSearcher::init(Wt::Dbo::Session& session, } -void -FeaturesSearcher::saveToCache() const -{ - if (!networkToCacheFile(_network, getCacheNetworkFilePath()) - || !objectPositionToCacheFile(_trackPosition, getCacheTrackPositionsFilePath())) - { - LMS_LOG(SIMILARITY, ERROR) << "Failed cache data"; - clearCache(); - } -} - -void -FeaturesSearcher::clearCache() const -{ - for (boost::filesystem::directory_iterator itEnd, it(getCacheDirectory()); it != itEnd; ++it) - boost::filesystem::remove_all(it->path()); -} - - static std::set getMatchingRefVectorsPosition(const std::set& ids, const std::map>& objectPosition) @@ -555,16 +372,19 @@ FeaturesSearcher::getSimilarObjects(const std::set& ids, { std::vector res; - auto now = std::chrono::system_clock::now(); - std::mt19937 randGenerator(std::chrono::duration_cast(now.time_since_epoch()).count()); + if (!isValid()) + return res; - std::set searchedRefVectorsPosition = getMatchingRefVectorsPosition(ids, objectPosition); + auto now {std::chrono::system_clock::now()}; + std::mt19937 randGenerator{static_cast(std::chrono::duration_cast(now.time_since_epoch()).count())}; + + std::set searchedRefVectorsPosition {getMatchingRefVectorsPosition(ids, objectPosition)}; if (searchedRefVectorsPosition.empty()) return res; while (1) { - std::set closestObjectIds = getObjectsIds(searchedRefVectorsPosition, objectsMap); + std::set closestObjectIds {getObjectsIds(searchedRefVectorsPosition, objectsMap)}; // Remove objects that are already in input or already reported for (auto id : ids) @@ -574,8 +394,7 @@ FeaturesSearcher::getSimilarObjects(const std::set& ids, closestObjectIds.erase(id); { - std::vector objectIdsToAdd(closestObjectIds.begin(), closestObjectIds.end()); - + std::vector objectIdsToAdd {closestObjectIds.begin(), closestObjectIds.end()}; std::shuffle(objectIdsToAdd.begin(), objectIdsToAdd.end(), randGenerator); std::copy(objectIdsToAdd.begin(), objectIdsToAdd.end(), std::back_inserter(res)); } @@ -587,7 +406,7 @@ FeaturesSearcher::getSimilarObjects(const std::set& ids, break; // If there is not enough objects, try again with closest neighbour until there is too much distance - auto closestRefVectorPosition = _network.getClosestRefVectorPosition(searchedRefVectorsPosition, _networkRefVectorsDistanceMedian * 0.75); + boost::optional closestRefVectorPosition {_network->getClosestRefVectorPosition(searchedRefVectorsPosition, _networkRefVectorsDistanceMedian * 0.75)}; if (!closestRefVectorPosition) break; diff --git a/src/similarity/features/SimilarityFeaturesSearcher.hpp b/src/similarity/features/SimilarityFeaturesSearcher.hpp index 72ccd932..6b97fa8b 100644 --- a/src/similarity/features/SimilarityFeaturesSearcher.hpp +++ b/src/similarity/features/SimilarityFeaturesSearcher.hpp @@ -26,17 +26,22 @@ #include "database/Types.hpp" #include "som/DataNormalizer.hpp" #include "som/Network.hpp" +#include "SimilarityFeaturesCache.hpp" namespace Similarity { + class FeaturesSearcher { public: - bool init(Wt::Dbo::Session& session, bool& stopRequested); - bool initFromCache(Wt::Dbo::Session& session); + // Use cache + FeaturesSearcher(Wt::Dbo::Session& session, FeaturesCache cache); - static void invalidateCache(); + // Use training (may be very slow) + FeaturesSearcher(Wt::Dbo::Session& session, bool& stopRequested); + + bool isValid() const; std::vector getSimilarTracks(const std::set& tracksId, std::size_t maxCount) const; std::vector getSimilarReleases(Database::IdType releaseId, std::size_t maxCount) const; @@ -44,31 +49,32 @@ class FeaturesSearcher void dump(Wt::Dbo::Session& session, std::ostream& os) const; + FeaturesCache toCache() const; + private: + using ObjectPositions = std::map>; + void init(Wt::Dbo::Session& session, SOM::Network network, - std::map> tracksPosition); - - void saveToCache() const; - void clearCache() const; + ObjectPositions tracksPosition); std::vector getSimilarObjects(const std::set& ids, const SOM::Matrix>& objectsMap, - const std::map>& objectPosition, + const ObjectPositions& objectPosition, std::size_t maxCount) const; - SOM::Network _network; - double _networkRefVectorsDistanceMedian = 0; + std::unique_ptr _network; + double _networkRefVectorsDistanceMedian {}; - SOM::Matrix> _artistsMap; - std::map> _artistPosition; + SOM::Matrix> _artistsMap; + ObjectPositions _artistPositions; - SOM::Matrix> _releasesMap; - std::map> _releasePosition; + SOM::Matrix> _releasesMap; + ObjectPositions _releasePositions; - SOM::Matrix> _tracksMap; - std::map> _trackPosition; + SOM::Matrix> _tracksMap; + ObjectPositions _trackPositions; }; diff --git a/src/similarity/features/som/DataNormalizer.cpp b/src/similarity/features/som/DataNormalizer.cpp index 6f615704..d6498fcf 100644 --- a/src/similarity/features/som/DataNormalizer.cpp +++ b/src/similarity/features/som/DataNormalizer.cpp @@ -31,14 +31,14 @@ static T variance(const std::vector& vec) { - std::size_t size = vec.size(); + std::size_t size {vec.size()}; if (size == 1) - return T{0.}; + return T {}; - T mean = std::accumulate(vec.begin(), vec.end(), T{0.}) / size; + const T mean {std::accumulate(vec.begin(), vec.end(), T{}) / size}; - return std::accumulate(vec.begin(), vec.end(), T{0.}, + return std::accumulate(vec.begin(), vec.end(), T {}, [mean, size] (T accumulator, const T& val) { return accumulator + ((val - mean) * (val - mean) / (size - 1)); @@ -46,7 +46,7 @@ variance(const std::vector& vec) } DataNormalizer::DataNormalizer(std::size_t inputDimCount) -: _inputDimCount(inputDimCount) +: _inputDimCount{inputDimCount} { } @@ -66,13 +66,13 @@ void DataNormalizer::computeNormalizationFactors(const std::vector& inputVectors) { if (inputVectors.empty()) - throw SOMException("Empty input vectors"); + throw Exception("Empty input vectors"); // For each dimension of the input, compute the min/max _minmax.clear(); _minmax.resize(_inputDimCount); - for (std::size_t dimId = 0; dimId < _inputDimCount; ++dimId) + for (std::size_t dimId {}; dimId < _inputDimCount; ++dimId) { std::vector values; @@ -82,7 +82,7 @@ DataNormalizer::computeNormalizationFactors(const std::vector& inpu values.push_back(inputVector[dimId]); } - auto result = std::minmax_element(values.begin(), values.end()); + auto result {std::minmax_element(values.begin(), values.end())}; _minmax[dimId] = {*result.first, *result.second}; } } @@ -104,7 +104,7 @@ DataNormalizer::normalizeData(InputVector& a) const { checkSameDimensions(a, _inputDimCount); - for (std::size_t dimId = 0; dimId < _inputDimCount; ++dimId) + for (std::size_t dimId {}; dimId < _inputDimCount; ++dimId) { a[dimId] = normalizeValue(a[dimId], dimId); } @@ -113,7 +113,7 @@ DataNormalizer::normalizeData(InputVector& a) const void DataNormalizer::dump(std::ostream& os) const { - for (std::size_t i = 0; i < _inputDimCount; ++i) + for (std::size_t i {}; i < _inputDimCount; ++i) os << "(" << _minmax[i].min << ", " << _minmax[i].max << ")"; } diff --git a/src/similarity/features/som/DataNormalizer.hpp b/src/similarity/features/som/DataNormalizer.hpp index 8f6219c7..96006556 100644 --- a/src/similarity/features/som/DataNormalizer.hpp +++ b/src/similarity/features/som/DataNormalizer.hpp @@ -53,7 +53,7 @@ class DataNormalizer private: InputVector::value_type normalizeValue(InputVector::value_type value, std::size_t dimensionId) const; - std::size_t _inputDimCount; + const std::size_t _inputDimCount; std::vector _minmax; // Indexed min/max used to normalize data }; diff --git a/src/similarity/features/som/InputVector.hpp b/src/similarity/features/som/InputVector.hpp new file mode 100644 index 00000000..68d5834e --- /dev/null +++ b/src/similarity/features/som/InputVector.hpp @@ -0,0 +1,194 @@ + +/* + * 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 + +namespace SOM +{ + +class Exception : public LmsException +{ + public: + Exception(const std::string& msg) : LmsException(msg) {} +}; + +class InputVector +{ + public: + using value_type = double; + using Norm = double; + using Distance = double; + + InputVector(std::size_t nbDimensions, value_type defaultValue = value_type {}) : _values(nbDimensions, defaultValue) {} + + bool hasSameDimension(const InputVector& other) const + { + return _values.size() == other._values.size(); + } + + std::size_t getNbDimensions() const + { + return _values.size(); + } + + value_type& operator[](std::size_t index) + { + if (index >= getNbDimensions()) + throw Exception("Bad range"); + + return _values[index]; + } + + value_type operator[](std::size_t index) const + { + if (index >= getNbDimensions()) + throw Exception("Bad range"); + + return _values[index]; + } + + InputVector& operator+=(const InputVector& other) + { + if (!hasSameDimension(other.getNbDimensions())) + throw Exception {"Not the same dimension count"}; + + for (std::size_t i {}; i < _values.size(); ++i) + { + _values[i] += other[i]; + } + + return *this; + } + + InputVector& operator-=(const InputVector& other) + { + if (!hasSameDimension(other.getNbDimensions())) + throw Exception {"Not the same dimension count"}; + + for (std::size_t i {}; i < _values.size(); ++i) + { + _values[i] -= other[i]; + } + + return *this; + } + + InputVector& operator*=(value_type factor) + { + for (std::size_t i {}; i < _values.size(); ++i) + { + _values[i] *= factor; + } + + return *this; + } + + Norm computeNorm() const + { + Norm res {}; + for (value_type val : _values) + res += val * val; + return std::sqrt(res); + } + + Distance computeEuclidianSquareDistance(const InputVector& other, const InputVector& weights) const + { + if (!hasSameDimension(other.getNbDimensions()) + || !hasSameDimension(weights.getNbDimensions())) + { + throw Exception {"Not the same dimension count"}; + } + + Distance res {}; + + for (std::size_t i {}; i < getNbDimensions(); ++i) + { + const InputVector::value_type diff {_values[i] - other._values[i]}; + res += diff * diff * weights._values[i]; + } + + return res; + } + + std::vector::iterator begin() + { + return _values.begin(); + } + + std::vector::const_iterator begin() const + { + return _values.cbegin(); + } + + std::vector::const_iterator cbegin() const + { + return _values.cbegin(); + } + + std::vector::iterator end() + { + return _values.end(); + } + + std::vector::const_iterator end() const + { + return _values.cend(); + } + + std::vector::const_iterator cend() const + { + return _values.cend(); + } + + private: + + friend class InputVector operator-(const InputVector& a, const InputVector& b) + { + if (!a.hasSameDimension(b.getNbDimensions())) + throw Exception {"Not the same dimension count"}; + + InputVector res {a.getNbDimensions()}; + + for (std::size_t i {}; i < res._values.size(); ++i) + res._values[i] = a._values[i] - b._values[i]; + + return res; + } + + friend std::ostream& + operator<<(std::ostream& os, const InputVector& a) + { + os << "["; + for (value_type val : a._values) + { + os << val << " "; + } + os << "]"; + + return os; + } + + std::vector _values; +}; + +} diff --git a/src/similarity/features/som/Matrix.hpp b/src/similarity/features/som/Matrix.hpp index 58155f52..32df1bff 100644 --- a/src/similarity/features/som/Matrix.hpp +++ b/src/similarity/features/som/Matrix.hpp @@ -28,6 +28,7 @@ namespace SOM { using Coordinate = unsigned; +using Norm = InputVector::value_type; struct Position { @@ -56,18 +57,18 @@ class Matrix Matrix() = default; Matrix(Coordinate width, Coordinate height) - : _width(width), - _height(height) + : _width{width}, + _height{height} { _values.resize(_width*_height); } - Matrix(std::size_t width, std::size_t height, std::vector values) - : _width(width), - _height(height), - _values(std::move(values)) + template + Matrix(Coordinate width, Coordinate height, CtArgs... args) + : _width{width}, + _height{height} { - assert(_values.size() == _width * _height); + _values.resize(_width*_height, T{args...}); } void clear() @@ -101,17 +102,17 @@ class Matrix { assert(!_values.empty()); - auto it = std::min_element(_values.begin(), _values.end(), func); - auto index = static_cast(std::distance(_values.begin(), it)); + auto it {std::min_element(_values.begin(), _values.end(), std::move(func))}; + auto index {static_cast(std::distance(_values.begin(), it))}; return {index % _height, index / _height}; } private: - Coordinate _width = 0; - Coordinate _height = 0; - std::vector _values; + Coordinate _width {}; + Coordinate _height {}; + std::vector _values; }; } // ns SOM diff --git a/src/similarity/features/som/Network.cpp b/src/similarity/features/som/Network.cpp index 30e21879..5c61e649 100644 --- a/src/similarity/features/som/Network.cpp +++ b/src/similarity/features/som/Network.cpp @@ -33,151 +33,69 @@ namespace SOM void checkSameDimensions(const InputVector& a, const InputVector& b) { - if (a.size() != b.size()) - throw SOMException("Bad data dimension count"); + if (!a.hasSameDimension(b)) + throw Exception("Bad data dimension count"); } void checkSameDimensions(const InputVector& a, std::size_t inputDimCount) { - if (a.size() != inputDimCount) - throw SOMException("Bad data dimension count"); + if (a.getNbDimensions() != inputDimCount) + throw Exception("Bad data dimension count"); } -static FeatureType +static LearningFactor defaultLearningFactor(Network::CurrentIteration iteration) { - constexpr FeatureType initialValue = 1; + static const LearningFactor initialValue{1}; - return initialValue * exp(-((iteration.idIteration + 1) / static_cast(iteration.iterationCount))); + return initialValue * exp(-((iteration.idIteration + 1) / static_cast(iteration.iterationCount))); } -static FeatureType +static InputVector::Distance euclidianSquareDistance(const InputVector& a, const InputVector& b, const InputVector& weights) { - checkSameDimensions(a, b); - checkSameDimensions(a, weights); - - FeatureType res = 0; - - for (std::size_t i = 0; i < a.size(); ++i) - { - res += (a[i] - b[i]) * (a[i] - b[i]) * weights[i]; - } - - return res; + return a.computeEuclidianSquareDistance(b, weights); } static -FeatureType +InputVector::value_type sigmaFunc(Network::CurrentIteration iteration) { - constexpr FeatureType sigma0 = 1; + constexpr InputVector::value_type sigma0 {1}; - return sigma0 * std::exp(- ((iteration.idIteration + 1) / static_cast(iteration.iterationCount))); + return sigma0 * std::exp(- ((iteration.idIteration + 1) / static_cast(iteration.iterationCount))); } static -FeatureType -defaultNeighbourhoodFunc(FeatureType norm, Network::CurrentIteration iteration) +InputVector::value_type +defaultNeighbourhoodFunc(Norm norm, const Network::CurrentIteration& iteration) { - auto sigma = sigmaFunc(iteration); + InputVector::value_type sigma {sigmaFunc(iteration)}; 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 -FeatureType -norm(const InputVector& a) -{ - FeatureType res = 0; - - for (auto val : a) - res += val * val; - - return std::sqrt(res); -} - -static -InputVector -operator+(const InputVector& a, const InputVector& b) -{ - checkSameDimensions(a, b); - - InputVector res; - res.reserve(a.size()); - - for (std::size_t dimId = 0; dimId < a.size(); ++dimId) - res.push_back(a[dimId] + b[dimId]); - - return res; -} - -static -InputVector -operator-(const InputVector& a, const InputVector& b) -{ - checkSameDimensions(a, b); - - InputVector res; - res.reserve(a.size()); - - for (std::size_t dimId = 0; dimId < a.size(); ++dimId) - res.push_back(a[dimId] - b[dimId]); - - return res; -} - -static -InputVector -operator*(const InputVector& a, FeatureType factor) -{ - InputVector res; - res.reserve(a.size()); - - for (std::size_t dimId = 0; dimId < a.size(); ++dimId) - res.push_back(a[dimId] * factor); - - return res; -} - Network::Network(Coordinate width, Coordinate height, std::size_t inputDimCount) : _inputDimCount(inputDimCount), -_weights(inputDimCount, static_cast(1)), -_refVectors(width, height), +_weights(inputDimCount, static_cast(1)), +_refVectors(width, height, _inputDimCount), _distanceFunc(euclidianSquareDistance), _learningFactorFunc(defaultLearningFactor), _neighbourhoodFunc(defaultNeighbourhoodFunc) { - auto now = std::chrono::system_clock::now(); - std::mt19937 randGenerator(std::chrono::duration_cast(now.time_since_epoch()).count()); + auto now {std::chrono::system_clock::now()}; + std::mt19937 randGenerator {static_cast(std::chrono::duration_cast(now.time_since_epoch()).count())}; // init each vector with a random normalized value - std::uniform_real_distribution dist(0, 1); + std::uniform_real_distribution dist{0, 1}; - for (Coordinate y = 0; y < _refVectors.getHeight(); ++y) + for (Coordinate y {}; y < _refVectors.getHeight(); ++y) { - for (Coordinate x = 0; x < _refVectors.getWidth(); ++x) + for (Coordinate x {}; x < _refVectors.getWidth(); ++x) { - auto& refVector = _refVectors.get({x,y}); - refVector.resize(_inputDimCount); - for (auto& val : refVector) + for (InputVector::value_type& val : _refVectors.get({x,y})) val = dist(randGenerator); } } @@ -199,25 +117,25 @@ Network::setRefVector(const Position& position, const InputVector& data) _refVectors[position] = data; } -double +InputVector::Distance Network::getRefVectorsDistance(const Position& position1, const Position& position2) const { return _distanceFunc(_refVectors.get(position1), _refVectors.get(position2), _weights); } -double +InputVector::Distance Network::computeRefVectorsDistanceMean() const { - std::vector values; - values.reserve(2*_refVectors.getHeight()*_refVectors.getWidth() - _refVectors.getWidth() - _refVectors.getHeight()); - for (Coordinate y = 0; y < _refVectors.getHeight(); ++y) + std::vector values; + values.reserve(2 * _refVectors.getHeight()*_refVectors.getWidth() - _refVectors.getWidth() - _refVectors.getHeight()); + for (Coordinate y {}; y < _refVectors.getHeight(); ++y) { - for (Coordinate x = 0; x < _refVectors.getWidth(); ++x) + for (Coordinate x {}; x < _refVectors.getWidth(); ++x) { if (x != _refVectors.getWidth() - 1) - values.push_back(getRefVectorsDistance( {x, y}, {x + 1, y})); + values.emplace_back(getRefVectorsDistance( {x, y}, {x + 1, y})); if (y != _refVectors.getHeight() - 1) - values.push_back(getRefVectorsDistance( {x, y}, {x, y + 1})); + values.emplace_back(getRefVectorsDistance( {x, y}, {x, y + 1})); } } @@ -227,20 +145,22 @@ Network::computeRefVectorsDistanceMean() const double Network::computeRefVectorsDistanceMedian() const { - std::vector values; + std::vector values; values.reserve(2*_refVectors.getHeight()*_refVectors.getWidth() - _refVectors.getWidth() - _refVectors.getHeight()); - for (Coordinate y = 0; y < _refVectors.getHeight(); ++y) + for (Coordinate y {}; y < _refVectors.getHeight(); ++y) { - for (Coordinate x = 0; x < _refVectors.getWidth(); ++x) + for (Coordinate x {}; x < _refVectors.getWidth(); ++x) { if (x != _refVectors.getWidth() - 1) - values.push_back(getRefVectorsDistance( {x, y}, {x + 1, y})); + values.emplace_back(getRefVectorsDistance( {x, y}, {x + 1, y})); if (y != _refVectors.getHeight() - 1) - values.push_back(getRefVectorsDistance( {x, y}, {x, y + 1})); + values.emplace_back(getRefVectorsDistance( {x, y}, {x, y + 1})); } } - return values[values.size()/2 - 1]; + std::sort(values.begin(), values.end()); + + return values[values.size() > 1 ? values.size()/2 - 1 : 0]; } void @@ -248,9 +168,9 @@ Network::dump(std::ostream& os) const { os << "Width: " << _refVectors.getWidth() << ", Height: " << _refVectors.getHeight() << std::endl;; - for (Coordinate y = 0; y < _refVectors.getHeight(); ++y) + for (Coordinate y {}; y < _refVectors.getHeight(); ++y) { - for (Coordinate x = 0; x < _refVectors.getWidth(); ++x) + for (Coordinate x {}; x < _refVectors.getWidth(); ++x) { os << _refVectors.get({x, y}) << " "; } @@ -270,21 +190,18 @@ Network::getClosestRefVectorPosition(const InputVector& data) const } boost::optional -Network::getClosestRefVectorPosition(const InputVector& data, double maxDistance) const +Network::getClosestRefVectorPosition(const InputVector& data, InputVector::Distance maxDistance) const { - Position position = _refVectors.getPositionMinElement([&](const auto& a, const auto& b) - { - return (_distanceFunc(a, data, _weights) < _distanceFunc(b, data, _weights)); - }); + boost::optional position {getClosestRefVectorPosition(data)}; - if (_distanceFunc(data, _refVectors.get(position), _weights) > maxDistance) - return boost::none; + if (_distanceFunc(data, _refVectors.get(*position), _weights) > maxDistance) + position.reset(); return position; } boost::optional -Network::getClosestRefVectorPosition(const std::set& refVectorsPosition, double maxDistance) const +Network::getClosestRefVectorPosition(const std::set& refVectorsPosition, InputVector::Distance maxDistance) const { std::set neighboursPosition; for (const Position& refVectorPosition : refVectorsPosition) @@ -322,49 +239,47 @@ Network::getClosestRefVectorPosition(const std::set& refVectorsPositio return (this->getRefVectorsDistance(a, neighbourPosition) < this->getRefVectorsDistance(b, neighbourPosition)); }); - double distance = getRefVectorsDistance(neighbourPosition, *min); + InputVector::Distance distance {getRefVectorsDistance(neighbourPosition, *min)}; if (distance > maxDistance) continue; - neighboursInfo.push_back({neighbourPosition, distance}); + neighboursInfo.emplace_back(NeighbourInfo {neighbourPosition, distance}); } if (neighboursInfo.empty()) return boost::none; - auto min = std::min_element(neighboursInfo.begin(), neighboursInfo.end(), + auto min {std::min_element(neighboursInfo.begin(), neighboursInfo.end(), [&](const auto& a, const auto& b) { return a.distance < b.distance; - }); + })}; return min->position; } -static FeatureType -computePositionNorm(Position c1, Position c2) +static Norm +computePositionNorm(const Position& c1, const Position& 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); + return std::sqrt((c1.x - c2.x) * (c1.x - c2.x) + (c1.y - c2.y) * (c1.y - c2.y)); } - void -Network::updateRefVectors(const Position& closestRefVectorPosition, const InputVector& input, FeatureType learningFactor, const CurrentIteration& iteration) +Network::updateRefVectors(const Position& closestRefVectorPosition, const InputVector& input, LearningFactor learningFactor, const CurrentIteration& iteration) { - for (Coordinate y = 0; y < _refVectors.getHeight(); ++y) + for (Coordinate y {}; y < _refVectors.getHeight(); ++y) { - for (Coordinate x = 0; x < _refVectors.getWidth(); ++x) + for (Coordinate x {}; x < _refVectors.getWidth(); ++x) { - auto& refVector = _refVectors.get({x, y}); + InputVector& refVector {_refVectors.get({x, y})}; - auto delta = input - refVector; - auto n = computePositionNorm({x, y}, closestRefVectorPosition); + const Norm norm {computePositionNorm({x, y}, closestRefVectorPosition)}; - refVector = refVector + delta * (learningFactor * _neighbourhoodFunc(n, iteration)); + InputVector delta {input - refVector}; + delta *= (learningFactor * _neighbourhoodFunc(norm, iteration)); + + refVector += delta; // * (learningFactor * _neighbourhoodFunc(norm, iteration)); } } } @@ -372,26 +287,26 @@ Network::updateRefVectors(const Position& closestRefVectorPosition, const InputV void Network::train(const std::vector& inputData, std::size_t nbIterations, ProgressCallback progressCallback, RequestStopCallback requestStopCallback) { - bool stopRequested{false}; + bool stopRequested {false}; std::vector inputDataShuffled; inputDataShuffled.reserve(inputData.size()); for (const auto& input : inputData) inputDataShuffled.push_back(&input); - auto now = std::chrono::system_clock::now(); - std::mt19937 randGenerator(std::chrono::duration_cast(now.time_since_epoch()).count()); + auto now {std::chrono::system_clock::now()}; + std::mt19937 randGenerator{static_cast(std::chrono::duration_cast(now.time_since_epoch()).count())}; - for (std::size_t i = 0; i < nbIterations; ++i) + for (std::size_t i {}; i < nbIterations; ++i) { - CurrentIteration curIter{i, nbIterations}; + CurrentIteration curIter {i, nbIterations}; if (progressCallback) progressCallback(curIter); std::shuffle(inputDataShuffled.begin(), inputDataShuffled.end(), randGenerator); - const auto learningFactor = _learningFactorFunc(curIter); + const LearningFactor learningFactor {_learningFactorFunc(curIter)}; for (const InputVector* input : inputDataShuffled) { diff --git a/src/similarity/features/som/Network.hpp b/src/similarity/features/som/Network.hpp index ce100e35..2d8ddf92 100644 --- a/src/similarity/features/som/Network.hpp +++ b/src/similarity/features/som/Network.hpp @@ -26,40 +26,29 @@ #include -#include "Matrix.hpp" - #include "utils/Exception.hpp" +#include "InputVector.hpp" +#include "Matrix.hpp" namespace SOM { -using FeatureType = double; -using InputVector = std::vector; +using LearningFactor = InputVector::value_type; + 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) {} -}; - class Network { public: - Network() = default; - // Init a network with random values Network(Coordinate width, Coordinate height, std::size_t inputDimCount); - // Init a network with serialized values - Network(const std::string& data); - - std::size_t getWidth() const { return _refVectors.getWidth(); } - std::size_t getHeight() const { return _refVectors.getHeight(); } + Coordinate getWidth() const { return _refVectors.getWidth(); } + Coordinate getHeight() const { return _refVectors.getHeight(); } std::size_t getInputDimCount() const { return _inputDimCount; } const InputVector& getDataWeights() const { return _weights; } @@ -81,14 +70,14 @@ class Network const InputVector& getRefVector(const Position& position) const; Position getClosestRefVectorPosition(const InputVector& data) const; - boost::optional getClosestRefVectorPosition(const InputVector& data, double maxDistance) const; + boost::optional getClosestRefVectorPosition(const InputVector& data, InputVector::Distance maxDistance) const; - boost::optional getClosestRefVectorPosition(const std::set& refVectorsPosition, double maxDistance) const; + boost::optional getClosestRefVectorPosition(const std::set& refVectorsPosition, InputVector::Distance maxDistance) const; - double getRefVectorsDistance(const Position& position1, const Position& position2) const; + InputVector::Distance getRefVectorsDistance(const Position& position1, const Position& position2) const; - double computeRefVectorsDistanceMean() const; - double computeRefVectorsDistanceMedian() const; + InputVector::Distance computeRefVectorsDistanceMean() const; + InputVector::Distance computeRefVectorsDistanceMedian() const; void dump(std::ostream& os) const; @@ -96,20 +85,21 @@ class Network // i is the current iteration // refVector(i+1) = refVector(i) + LearningFactor(i) * NeighbourhoodFunc(i) * (MatchingRefVector - refVector) - using DistanceFunc = std::function; + using DistanceFunc = std::function; void setDistanceFunc(DistanceFunc distanceFunc); + DistanceFunc getDistanceFunc() { return _distanceFunc; } - using LearningFactorFunc = std::function; + using LearningFactorFunc = std::function; void setLearningFactorFunc(LearningFactorFunc learningFactorFunc); - using NeighbourhoodFunc = std::function; + using NeighbourhoodFunc = std::function; void setNeighbourhoodFunc(NeighbourhoodFunc neighbourhoodFunc); private: - void updateRefVectors(const Position& closestRefVectorPosition, const InputVector& input, FeatureType learningFactor, const CurrentIteration& iteration); + void updateRefVectors(const Position& closestRefVectorPosition, const InputVector& input, LearningFactor learningFactor, const CurrentIteration& iteration); - std::size_t _inputDimCount = 0; + std::size_t _inputDimCount {}; InputVector _weights; // weight for each dimension Matrix _refVectors; diff --git a/test/Makefile.am b/test/Makefile.am index 8dbec7c5..06f44b52 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -1,7 +1,14 @@ -TESTS = +TESTS = som-test -check_PROGRAMS = +check_PROGRAMS = som-test + +som_test_SOURCES = \ + $(srcdir)/som-test/SomTest.cpp \ + $(top_srcdir)/src/similarity/features/som/DataNormalizer.cpp \ + $(top_srcdir)/src/similarity/features/som/Network.cpp + +som_test_CXXFLAGS=-std=c++14 -Wall -I${top_srcdir}/src/ -I${top_srcdir}/src/similarity/features/som/ diff --git a/tools/Makefile.am b/tools/Makefile.am index c3c14142..52252310 100644 --- a/tools/Makefile.am +++ b/tools/Makefile.am @@ -1,2 +1,2 @@ -SUBDIRS = feature-extractor metadata +SUBDIRS = similarity metadata diff --git a/tools/feature-extractor/LmsFeatureExtractor.cpp b/tools/similarity/LmsSimilarity.cpp similarity index 72% rename from tools/feature-extractor/LmsFeatureExtractor.cpp rename to tools/similarity/LmsSimilarity.cpp index f994be65..d840b736 100644 --- a/tools/feature-extractor/LmsFeatureExtractor.cpp +++ b/tools/similarity/LmsSimilarity.cpp @@ -34,11 +34,9 @@ std::ostream& operator<<(std::ostream& os, const Database::Track::pointer& track } static -std::vector -getTrackFeatures(Wt::Dbo::Session &session, Database::Track::pointer track, const std::map& featuresSettings) +bool +getTrackFeatures(Wt::Dbo::Session &session, const Database::Track::pointer& track, const std::map& featuresSettings, SOM::InputVector& res) { - std::vector res; - std::map> features; for (const auto& featureSettings : featuresSettings) features[featureSettings.first] = {}; @@ -46,22 +44,21 @@ getTrackFeatures(Wt::Dbo::Session &session, Database::Track::pointer track, cons if (!track->getTrackFeatures()->getFeatures(features)) { std::cout << "Skipping track '" << track->getMBID() << "': missing item" << std::endl; - return res; + return false; }; + std::size_t index {}; for (const auto& feature : features) { auto it = featuresSettings.find(feature.first); if (it == featuresSettings.end() || (feature.second.size() != it->second)) - { - res.clear(); - break; - } + return false; - res.insert( res.end(), feature.second.begin(), feature.second.end() ); + for (double value : feature.second) + res[index++] = value; } - return res; + return true; } @@ -69,10 +66,10 @@ int main(int argc, char *argv[]) { try { - const std::size_t width = 15; - const std::size_t height = 15; - const std::size_t nbIterations = 2; - const std::size_t nbTracks = 5000; + const std::size_t width = 10; + const std::size_t height = 10; + const std::size_t nbIterations = 20; + std::size_t nbTracks = 5000; const std::map featuresSettings = { @@ -91,7 +88,6 @@ int main(int argc, char *argv[]) nbDims += featureSettings.second; boost::filesystem::path configFilePath = "/etc/lms.conf"; - if (argc >= 2) configFilePath = std::string(argv[1], 0, 256); @@ -104,38 +100,32 @@ int main(int argc, char *argv[]) std::cout << "Getting all features..." << std::endl; Wt::Dbo::Transaction transaction(db.getSession()); - auto tracks = Database::Track::getAllWithFeatures(db.getSession()); + auto tracks = Database::Track::getAllWithFeatures(db.getSession(), nbTracks); - std::cout << "Getting all features DONE" << std::endl; - -/* auto now = std::chrono::system_clock::now(); - std::mt19937 randGenerator(std::chrono::duration_cast(now.time_since_epoch()).count()); - std::shuffle(tracks.begin(), tracks.end(), randGenerator); -*/ - tracks.resize(nbTracks); + nbTracks = tracks.size(); + std::cout << "Getting features DONE (" << nbTracks << " tracks)" << std::endl; std::cout << "Reading features..." << std::endl; - std::vector< std::vector > tracksFeatures; + std::vector tracksFeatures; - for (auto track : tracks) + for (const auto& track : tracks) { - auto features = getTrackFeatures(db.getSession(), track, featuresSettings); - - if (features.empty()) + SOM::InputVector features {nbDims}; + if (!getTrackFeatures(db.getSession(), track, featuresSettings, features)) continue; tracksFeatures.emplace_back(std::move(features)); } std::cout << "Reading features DONE" << std::endl; - SOM::Network network(width, height, nbDims); - SOM::DataNormalizer normalizer(nbDims); + SOM::Network network {width, height, nbDims}; + SOM::DataNormalizer normalizer {nbDims}; - std::vector weights; + SOM::InputVector weights {nbDims}; for (const auto& featureSettings : featuresSettings) { - for (std::size_t i = 0; i < featureSettings.second; ++i) - weights.push_back(1. / featureSettings.second); + for (std::size_t i {}; i < featureSettings.second; ++i) + weights[i] = SOM::InputVector::value_type{1. / featureSettings.second}; } network.setDataWeights(weights); @@ -147,12 +137,17 @@ int main(int argc, char *argv[]) normalizer.dump(std::cout); std::cout << "Dumping normalizer DONE" << std::endl; - for (auto& features : tracksFeatures) + for (SOM::InputVector& features : tracksFeatures) normalizer.normalizeData(features); std::cout << "Normalizing DONE" << std::endl; + auto progress {[](const SOM::Network::CurrentIteration& iteration) + { + std::cout << "Iteration " << iteration.idIteration + 1 << " of " << iteration.iterationCount << std::endl;; + }}; + std::cout << "Training..." << std::endl; - network.train(tracksFeatures, nbIterations); + network.train(tracksFeatures, nbIterations, progress); std::cout << "Training DONE" << std::endl; auto meanDistance = network.computeRefVectorsDistanceMean(); @@ -160,20 +155,18 @@ int main(int argc, char *argv[]) auto medianDistance = network.computeRefVectorsDistanceMedian(); std::cout << "MEDIAN distance = " << medianDistance << std::endl; -#if 0 std::cout << "Classifying tracks..." << std::endl; SOM::Matrix< std::vector > tracksMap(width, height); for (auto track : tracks) { - auto features = getTrackFeatures(db.getSession(), track, featuresSettings); - - if (features.empty()) + SOM::InputVector features {nbDims}; + if (!getTrackFeatures(db.getSession(), track, featuresSettings, features)) continue; normalizer.normalizeData(features); - auto position = network.getClosestRefVectorPosition(features); + SOM::Position position = network.getClosestRefVectorPosition(features); tracksMap[position].push_back(track); } @@ -188,7 +181,7 @@ int main(int argc, char *argv[]) std::cout << "{" << x << ", " << y << "}" << std::endl; const auto& tracks = tracksMap[{x, y}]; - for (auto track : tracks) + for (const auto& track : tracks) { std::cout << " - " << track << std::endl; } @@ -196,39 +189,35 @@ int main(int argc, char *argv[]) } // For each track, get the nearest tracks - for (auto track : tracks) + for (const auto& track : tracks) { - auto features = getTrackFeatures(db.getSession(), track, featuresSettings); - - if (features.empty()) + SOM::InputVector features {nbDims}; + if (!getTrackFeatures(db.getSession(), track, featuresSettings, features)) continue; normalizer.normalizeData(features); - auto refVectorPosition = network.getClosestRefVectorPosition(features); + SOM::Position refVectorPosition {network.getClosestRefVectorPosition(features)}; std::cout << "Getting nearest songs for track " << track << " in {" << refVectorPosition.x << ", " << refVectorPosition.y << "}:" << std::endl; for (auto similarTrack : tracksMap[refVectorPosition]) std::cout << " - " << similarTrack << std::endl; - std::set neighbourPosition = {refVectorPosition}; - for (std::size_t i = 0; i < 5; ++i) + std::set neighbourPosition {refVectorPosition}; + for (std::size_t i {}; i < 3; ++i) { auto position = network.getClosestRefVectorPosition(neighbourPosition, medianDistance); if (!position) break; std::cout << " - in {" << position->x << ", " << position->y << "}, dist = " << network.getRefVectorsDistance(*position, refVectorPosition) << std::endl; - for (auto similarTrack : tracksMap[*position]) + for (const auto& similarTrack : tracksMap[*position]) std::cout << " - " << similarTrack << std::endl; neighbourPosition.insert(*position); } } -#endif - - std::cout << "Classifying tracks DONE" << std::endl; } catch( std::exception& e) { diff --git a/tools/feature-extractor/Makefile.am b/tools/similarity/Makefile.am similarity index 78% rename from tools/feature-extractor/Makefile.am rename to tools/similarity/Makefile.am index a6725f54..bc7eb69b 100644 --- a/tools/feature-extractor/Makefile.am +++ b/tools/similarity/Makefile.am @@ -1,7 +1,7 @@ -bin_PROGRAMS = lms-feature-extractor +bin_PROGRAMS = lms-similarity -lms_feature_extractor_SOURCES = \ - $(srcdir)/LmsFeatureExtractor.cpp \ +lms_similarity_SOURCES = \ + $(srcdir)/LmsSimilarity.cpp \ $(top_srcdir)/src/database/Artist.cpp \ $(top_srcdir)/src/database/Cluster.cpp \ $(top_srcdir)/src/database/DatabaseHandler.cpp \ @@ -18,5 +18,5 @@ lms_feature_extractor_SOURCES = \ $(top_srcdir)/src/utils/Logger.cpp \ $(top_srcdir)/src/utils/Utils.cpp -lms_feature_extractor_CXXFLAGS=-std=c++14 -Wall -I$(top_srcdir)/src -D_REENTRANT +lms_similarity_CXXFLAGS=-std=c++14 -Wall -I$(top_srcdir)/src -D_REENTRANT