diff --git a/configure.ac b/configure.ac index 69669837..4f4bee7f 100644 --- a/configure.ac +++ b/configure.ac @@ -16,7 +16,7 @@ fi AC_SUBST(MAGICKXX_CFLAGS) AC_SUBST(MAGICKXX_LIBS) -AC_CHECK_HEADERS([Wt/WApplication.h pstreams/pstream.h], +AC_CHECK_HEADERS([Wt/WApplication.h pstreams/pstream.h curl/curl.h], [], [AC_MSG_ERROR([Header not found or unusable !])]) @@ -76,12 +76,17 @@ AC_CHECK_LIB( [config++], , [AC_MSG_ERROR([libconfig++ not found!])]) +AC_CHECK_LIB( [curl], + [curl_easy_init], + , + [AC_MSG_ERROR([libcurl not found!])]) + AC_CONFIG_FILES([Makefile src/Makefile test/Makefile tools/Makefile tools/metadata/Makefile - tools/classifier/Makefile]) + tools/clusterer/Makefile]) AC_OUTPUT diff --git a/src/classifier/Clusterer.hpp b/src/clusterer/Clusterer.hpp similarity index 73% rename from src/classifier/Clusterer.hpp rename to src/clusterer/Clusterer.hpp index 7b9d31e5..809fd79e 100644 --- a/src/classifier/Clusterer.hpp +++ b/src/clusterer/Clusterer.hpp @@ -19,7 +19,7 @@ #pragma once - +#include #include "SOM.hpp" #include "DataNormalizer.hpp" @@ -31,9 +31,16 @@ class Clusterer { public: using SampleType = std::pair; + using Cluster = std::vector; + Clusterer(const std::vector& samples, std::size_t inputDimCount, std::size_t iterationCount); - const std::vector& getClusterValues(const SOM::InputVector& data) const; + const Cluster& getCluster(const SOM::InputVector& data) const; + + // Sorted results (best first) + std::vector getClusters(const SOM::InputVector& data, std::size_t nbClusters) const; + + const std::vector& getAllClusters() const; void dump(std::ostream& os) const; @@ -55,8 +62,8 @@ class Clusterer template Clusterer::Clusterer(const std::vector& samples, std::size_t inputDimCount, std::size_t iterationCount) : -_width(3), -_height(3), +_width(std::sqrt(samples.size()/20)), +_height(std::sqrt(samples.size()/20)), _dataNormalizer(inputDimCount), _network(_width, _height, inputDimCount) { @@ -116,8 +123,8 @@ Clusterer::train(const std::vector>& samples, } template -const std::vector& -Clusterer::getClusterValues(const SOM::InputVector& inputVector) const +const typename Clusterer::Cluster& +Clusterer::getCluster(const SOM::InputVector& inputVector) const { auto inputVectorNormalized = inputVector; _dataNormalizer.normalizeData(inputVectorNormalized); @@ -125,16 +132,42 @@ Clusterer::getClusterValues(const SOM::InputVector& inputVector) const return getValues(_network.classify(inputVectorNormalized)); } +template +std::vector::Cluster> +Clusterer::getClusters(const SOM::InputVector& inputVector, std::size_t nbClusters) const +{ + auto inputVectorNormalized = inputVector; + _dataNormalizer.normalizeData(inputVectorNormalized); + + std::vector::Cluster> res; + for (auto& cluster : _network.classify(inputVectorNormalized, nbClusters)) + { + res.push_back(getValues(cluster)); + } + + return res; +} + +template +const std::vector::Cluster>& +Clusterer::getAllClusters() const +{ + return _values; +} + template void Clusterer::dump(std::ostream& os) const { + os << "Normalizer:" << std::endl; + _dataNormalizer.dump(os); + os << std::endl; os << "Internal network:" << std::endl; _network.dump(os); os << "Values: " << std::endl; - for (std::size_t x = 0; x < _width; ++x) + for (std::size_t y = 0; y < _height; ++y) { - for (std::size_t y = 0; y < _height; ++y) + for (std::size_t x = 0; x < _width; ++x) { os << "["; for (const auto& value : getValues({x, y})) @@ -146,4 +179,3 @@ Clusterer::dump(std::ostream& os) const } - diff --git a/src/classifier/DataNormalizer.cpp b/src/clusterer/DataNormalizer.cpp similarity index 61% rename from src/classifier/DataNormalizer.cpp rename to src/clusterer/DataNormalizer.cpp index 38e85d05..56066618 100644 --- a/src/classifier/DataNormalizer.cpp +++ b/src/clusterer/DataNormalizer.cpp @@ -19,22 +19,42 @@ #include "DataNormalizer.hpp" -#include - #include +#include namespace SOM { +template +static +T +variance(const std::vector& vec) +{ + std::size_t size = vec.size(); + + if (size == 1) + return T{0.}; + + T mean = std::accumulate(vec.begin(), vec.end(), T{0.}) / size; + + return std::accumulate(vec.begin(), vec.end(), T{0.}, + [mean, size] (T accumulator, const T& val) + { + return accumulator + ((val - mean) * (val - mean) / (size - 1)); + }); +} + DataNormalizer::DataNormalizer(std::size_t inputDimCount) : _inputDimCount(inputDimCount) { } - void DataNormalizer::computeNormalizationFactors(const std::vector& inputVectors) { + if (inputVectors.empty()) + throw SOMException("Empty input vectors"); + // For each dimension of the input, compute the min/max _minmax.clear(); _minmax.resize(_inputDimCount); @@ -54,6 +74,18 @@ DataNormalizer::computeNormalizationFactors(const std::vector& inpu } } +InputVector::value_type +DataNormalizer::normalizeValue(InputVector::value_type value, std::size_t dimId) const +{ + // clamp + if (value > _minmax[dimId].max) + value = _minmax[dimId].max; + else if (value < _minmax[dimId].min) + value = _minmax[dimId].min; + + return (value - _minmax[dimId].min) / (_minmax[dimId].max - _minmax[dimId].min); +} + void DataNormalizer::normalizeData(InputVector& a) const { @@ -61,14 +93,15 @@ DataNormalizer::normalizeData(InputVector& a) const 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); + a[dimId] = normalizeValue(a[dimId], dimId); } } +void +DataNormalizer::dump(std::ostream& os) const +{ + for (std::size_t i = 0; i < _inputDimCount; ++i) + os << "(" << _minmax[i].min << ", " << _minmax[i].max << ")"; +} + } // namespace SOM diff --git a/src/classifier/DataNormalizer.hpp b/src/clusterer/DataNormalizer.hpp similarity index 86% rename from src/classifier/DataNormalizer.hpp rename to src/clusterer/DataNormalizer.hpp index be96dd36..216297c7 100644 --- a/src/classifier/DataNormalizer.hpp +++ b/src/clusterer/DataNormalizer.hpp @@ -19,6 +19,9 @@ #pragma once +#include +#include + #include "SOM.hpp" namespace SOM @@ -33,7 +36,11 @@ class DataNormalizer void normalizeData(InputVector& data) const; + void dump(std::ostream& os) const; + private: + InputVector::value_type normalizeValue(InputVector::value_type value, std::size_t dimensionId) const; + std::size_t _inputDimCount; struct minmax diff --git a/src/classifier/SOM.cpp b/src/clusterer/SOM.cpp similarity index 89% rename from src/classifier/SOM.cpp rename to src/clusterer/SOM.cpp index 32e1c67b..0914cc60 100644 --- a/src/classifier/SOM.cpp +++ b/src/clusterer/SOM.cpp @@ -204,7 +204,6 @@ Network::getRefVector(std::size_t x, std::size_t y) const return _refVectors[x + y*_width]; } - void Network::dump(std::ostream& os) const { @@ -242,6 +241,44 @@ Network::classify(const InputVector& data) const return getClosestRefVector(data); } +std::vector +Network::classify(const InputVector& data, std::size_t size) const +{ + struct Entry + { + Coords coords; + InputVector refVector; + }; + std::vector sortedEntries; + + for (std::size_t x = 0; x < _width; ++x) + { + for (std::size_t y = 0; y < _height; ++y) + { + sortedEntries.push_back( Entry{{x, y}, getRefVector(x, y)} ); + } + } + + const InputVector& closestRefVector = getRefVector(getClosestRefVector(data)); + + std::sort(sortedEntries.begin(), sortedEntries.end(), + [&](const Entry& a, const Entry& b) + { + return _distanceFunc(a.refVector, closestRefVector, _weights) < _distanceFunc(b.refVector, closestRefVector, _weights); + }); + + std::vector res; + for (const Entry& entry : sortedEntries) + { + res.push_back(entry.coords); + + if (res.size() == size) + break; + } + + return res; +} + static InputVector::value_type computeCoordsNorm(Coords c1, Coords c2) { diff --git a/src/classifier/SOM.hpp b/src/clusterer/SOM.hpp similarity index 93% rename from src/classifier/SOM.hpp rename to src/clusterer/SOM.hpp index 2d60ff47..e292b02d 100644 --- a/src/classifier/SOM.hpp +++ b/src/clusterer/SOM.hpp @@ -61,6 +61,9 @@ class Network // data must be normalized Coords classify(const InputVector& data) const; + // ordered from closest to farthest + std::vector classify(const InputVector& data, std::size_t size) const; + void dump(std::ostream& os) const; // For each ref vector, update formula is: @@ -86,6 +89,7 @@ class Network InputVector& getRefVector(std::size_t x, std::size_t y); const InputVector& getRefVector(std::size_t x, std::size_t y) const; + const InputVector& getRefVector(Coords coords) const { return getRefVector(coords.x, coords.y); } Coords getClosestRefVector(const InputVector& data) const; void updateRefVectors(Coords closestRefVectorCoords, const InputVector& input, Progress progress); diff --git a/src/database/DatabaseHandler.cpp b/src/database/DatabaseHandler.cpp index 4645d464..5ec865db 100644 --- a/src/database/DatabaseHandler.cpp +++ b/src/database/DatabaseHandler.cpp @@ -196,7 +196,7 @@ Handler::createConnectionPool(boost::filesystem::path p) auto connection = std::make_unique(p.string()); connection->executeSql("pragma journal_mode=WAL"); - connection->setProperty("show-queries", "true"); +// connection->setProperty("show-queries", "true"); auto pool = std::make_unique(std::move(connection), 1); pool->setTimeout(std::chrono::seconds(10)); diff --git a/tools/Makefile.am b/tools/Makefile.am index 16922b29..19a2adce 100644 --- a/tools/Makefile.am +++ b/tools/Makefile.am @@ -1,2 +1,2 @@ -SUBDIRS = metadata classifier +SUBDIRS = metadata clusterer diff --git a/tools/classifier/LmsClassifier.cpp b/tools/classifier/LmsClassifier.cpp deleted file mode 100644 index 1e619ce0..00000000 --- a/tools/classifier/LmsClassifier.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include - -#include -#include -#include - -#include "classifier/SOM.hpp" -#include "classifier/DataNormalizer.hpp" -#include "classifier/Clusterer.hpp" - -int main(int argc, char *argv[]) -{ - - if (argc != 2) - { - std::cerr << "Usage: " << std::endl; - return EXIT_FAILURE; - } - - auto iterationCount = std::stoul(argv[1]); - - std::vector< std::pair, std::string> > inputValues = - { - {{ 160, 1 }, { "banane" }}, - {{ 80, -1 }, { "poire" }}, - {{ 80, -0.75 }, {"pocolat"}}, - {{ 240, 0.5 }, {"abricot"}}, - {{ 240, -0.5 }, {"peche"}}, - {{ 120, -0.5 }, {"fraise"}}, - {{ 140, -0.5 }, {"myrtille"}}, - }; - - Clusterer classifier(inputValues, 2, iterationCount); - - std::cout << "Clusterer :" << std::endl; - classifier.dump(std::cout); - - std::cout << "Classify 195, 0.35 = " << std::endl; - for (const auto& val : classifier.getClusterValues({195, 0.35})) - std::cout << val << " " << std::endl; - - return EXIT_SUCCESS; -} - diff --git a/tools/classifier/Makefile.am b/tools/classifier/Makefile.am deleted file mode 100644 index 2d036d25..00000000 --- a/tools/classifier/Makefile.am +++ /dev/null @@ -1,11 +0,0 @@ -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 - diff --git a/tools/clusterer/LmsClusterer.cpp b/tools/clusterer/LmsClusterer.cpp new file mode 100644 index 00000000..7d077d98 --- /dev/null +++ b/tools/clusterer/LmsClusterer.cpp @@ -0,0 +1,209 @@ +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include "clusterer/SOM.hpp" +#include "clusterer/DataNormalizer.hpp" +#include "clusterer/Clusterer.hpp" +#include "database/DatabaseHandler.hpp" +#include "database/Track.hpp" +#include "database/Artist.hpp" +#include "database/Release.hpp" +#include "utils/Config.hpp" + +static std::vector features = +{ + "lowlevel.average_loudness", + "lowlevel.barkbands_flatness_db.mean", + "lowlevel.dissonance.mean", + "lowlevel.dynamic_complexity", + "lowlevel.hfc.mean", // GOOD + "lowlevel.melbands_crest.mean", + "lowlevel.melbands_kurtosis.mean", + "lowlevel.melbands_skewness.mean", + "lowlevel.melbands_spread.mean", + "lowlevel.pitch_salience.mean", + "lowlevel.pitch_salience.var", + "lowlevel.silence_rate_30dB.mean", + "lowlevel.silence_rate_60dB.mean", + "lowlevel.spectral_centroid.mean", + "lowlevel.spectral_complexity.mean", + "lowlevel.spectral_decrease.mean", + "lowlevel.spectral_energy.mean", + "lowlevel.spectral_energyband_high.mean", + "lowlevel.spectral_energyband_low.mean", + "lowlevel.spectral_energyband_middle_high.mean", + "lowlevel.spectral_energyband_middle_low.mean", + "lowlevel.spectral_entropy.mean", + "lowlevel.spectral_flux.mean", + "lowlevel.spectral_kurtosis.mean", + "lowlevel.spectral_rms.mean", + "lowlevel.spectral_skewness.mean", + "lowlevel.spectral_spread.mean", + "lowlevel.spectral_strongpeak.mean", + "lowlevel.zerocrossingrate.mean", + "rhythm.beats_loudness.mean", // BAD + "rhythm.bpm", + "tonal.chords_changes_rate", // OK +// "tonal.chords_number_rate", // BAD + "tonal.chords_strength.mean", // OK + "tonal.hpcp_entropy.mean", // GOOD +}; + +static size_t writeToFile(void *buffer, size_t size, size_t nmemb, void* ctx) +{ + std::ofstream& ofs = *reinterpret_cast(ctx); + + ofs.write(reinterpret_cast(buffer), size * nmemb); + + return size * nmemb; +} + +static void acousticBrainzGetLowLevel(const std::string& mbid, boost::filesystem::path output) +{ + std::string url = "http://acousticbrainz.org/api/v1/" + mbid + "/low-level"; + + std::cout << "GET " << url << std::endl; + + CURL *curl; + CURLcode res; + + curl = curl_easy_init(); + if (!curl) + { + return; + } + + std::ofstream ofs(output.string().c_str()); + if (!ofs) + { + curl_easy_cleanup(curl); + std::cerr << "Cannot open " << output.string() << " for writing purpose" << std::endl; + return; + } + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeToFile); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ofs); + + res = curl_easy_perform(curl); + + if (res != CURLE_OK) + { + std::cerr << "perform failed: " << curl_easy_strerror(res) << std::endl; + } + + curl_easy_cleanup(curl); +} + +static boost::filesystem::path getLowLevelFeaturePath(const std::string& mbid) +{ + return boost::filesystem::path(Config::instance().getPath("working-dir") / "features" / mbid); +} + +std::vector getFeatures(const std::string& mbid) +{ + + std::vector res; + + try + { + boost::property_tree::ptree root; + + boost::property_tree::read_json(getLowLevelFeaturePath(mbid).string(), root); + + for (const auto& feature : features) + { + res.push_back(root.get(feature)); + } + } + catch (std::exception& e) + { + std::cerr << "Caught exception during processing " << mbid << std::endl; + } + + return res; +} + +int main(int argc, char *argv[]) +{ + try + { + boost::filesystem::path configFilePath = "/etc/lms.conf"; + + if (argc >= 2) + configFilePath = std::string(argv[1], 0, 256); + + Config::instance().setFile(configFilePath); + + Database::Handler::configureAuth(); + auto connectionPool = Database::Handler::createConnectionPool(Config::instance().getPath("working-dir") / "lms.db"); + Database::Handler db(*connectionPool); + + Wt::Dbo::Transaction transaction(db.getSession()); + + auto tracks = Database::Track::getAll(db.getSession()); + + std::vector, Database::IdType>> entries; + + std::cout << "Constructing input vectors..." << std::endl; + + for (auto track : tracks) + { + if (track->getMBID().empty()) + continue; + + auto path = getLowLevelFeaturePath(track->getMBID()); + if (!boost::filesystem::exists(path)) + acousticBrainzGetLowLevel(track->getMBID(), path); + + if (!boost::filesystem::exists(path)) + continue; + + std::pair, Database::IdType> entry; + + entry.first = getFeatures(track->getMBID()); + entry.second = track.id(); + + if (entry.first.size() == features.size()) + entries.push_back(std::move(entry)); + } + + std::cout << "Constructing input vectors... DONE" << std::endl; + + std::cout << "Clutering..." << std::endl; + Clusterer clusterer(entries, features.size(), 500); + + std::cout << "Clusterer :" << std::endl; + clusterer.dump(std::cout); + std::cout << std::endl; + + for (const auto& cluster : clusterer.getAllClusters()) + { + std::cout << "******************" << std::endl; + for (const auto& value : cluster) + { + auto track = Database::Track::getById(db.getSession(), value); + auto artist = track->getArtist(); + auto release = track->getRelease(); + + std::cout << "\t" << value << " - " << (artist ? artist->getName() : "") << " - " << (release ? release->getName() : "" ) << " - " << track->getName() << std::endl; + } + std::cout << std::endl; + } + } + catch( std::exception& e) + { + std::cerr << "Caught exception: " << e.what() << std::endl; + } + + return EXIT_SUCCESS; +} + diff --git a/tools/clusterer/Makefile.am b/tools/clusterer/Makefile.am new file mode 100644 index 00000000..179ffc34 --- /dev/null +++ b/tools/clusterer/Makefile.am @@ -0,0 +1,21 @@ +bin_PROGRAMS = lms-clusterer + +lms_clusterer_SOURCES = \ + $(srcdir)/LmsClusterer.cpp \ + $(top_srcdir)/src/clusterer/DataNormalizer.cpp \ + $(top_srcdir)/src/clusterer/SOM.cpp \ + $(top_srcdir)/src/database/Artist.cpp \ + $(top_srcdir)/src/database/Cluster.cpp \ + $(top_srcdir)/src/database/DatabaseHandler.cpp \ + $(top_srcdir)/src/database/TrackList.cpp \ + $(top_srcdir)/src/database/Release.cpp \ + $(top_srcdir)/src/database/ScanSettings.cpp \ + $(top_srcdir)/src/database/SqlQuery.cpp \ + $(top_srcdir)/src/database/Track.cpp \ + $(top_srcdir)/src/database/User.cpp \ + $(top_srcdir)/src/utils/Config.cpp \ + $(top_srcdir)/src/utils/Logger.cpp \ + $(top_srcdir)/src/utils/Utils.cpp + +lms_clusterer_CXXFLAGS=-std=c++14 -Wall -I$(top_srcdir)/src -D_REENTRANT +