WIP on the clusterer

This commit is contained in:
emeric
2018-12-14 13:41:22 +01:00
parent de712936eb
commit b76c60f437
12 changed files with 372 additions and 79 deletions
+7 -2
View File
@@ -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
@@ -19,7 +19,7 @@
#pragma once
#include <cmath>
#include "SOM.hpp"
#include "DataNormalizer.hpp"
@@ -31,9 +31,16 @@ class Clusterer
{
public:
using SampleType = std::pair<SOM::InputVector /* key */, T /* value*/ >;
using Cluster = std::vector<T>;
Clusterer(const std::vector<SampleType>& samples, std::size_t inputDimCount, std::size_t iterationCount);
const std::vector<T>& getClusterValues(const SOM::InputVector& data) const;
const Cluster& getCluster(const SOM::InputVector& data) const;
// Sorted results (best first)
std::vector<Cluster> getClusters(const SOM::InputVector& data, std::size_t nbClusters) const;
const std::vector<Cluster>& getAllClusters() const;
void dump(std::ostream& os) const;
@@ -55,8 +62,8 @@ class Clusterer
template<typename T>
Clusterer<T>::Clusterer(const std::vector<SampleType>& 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<T>::train(const std::vector<std::pair<SOM::InputVector, T>>& samples,
}
template<typename T>
const std::vector<T>&
Clusterer<T>::getClusterValues(const SOM::InputVector& inputVector) const
const typename Clusterer<T>::Cluster&
Clusterer<T>::getCluster(const SOM::InputVector& inputVector) const
{
auto inputVectorNormalized = inputVector;
_dataNormalizer.normalizeData(inputVectorNormalized);
@@ -125,16 +132,42 @@ Clusterer<T>::getClusterValues(const SOM::InputVector& inputVector) const
return getValues(_network.classify(inputVectorNormalized));
}
template<typename T>
std::vector<typename Clusterer<T>::Cluster>
Clusterer<T>::getClusters(const SOM::InputVector& inputVector, std::size_t nbClusters) const
{
auto inputVectorNormalized = inputVector;
_dataNormalizer.normalizeData(inputVectorNormalized);
std::vector<typename Clusterer<T>::Cluster> res;
for (auto& cluster : _network.classify(inputVectorNormalized, nbClusters))
{
res.push_back(getValues(cluster));
}
return res;
}
template<typename T>
const std::vector<typename Clusterer<T>::Cluster>&
Clusterer<T>::getAllClusters() const
{
return _values;
}
template<typename T>
void
Clusterer<T>::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<T>::dump(std::ostream& os) const
}
@@ -19,22 +19,42 @@
#include "DataNormalizer.hpp"
#include <iostream>
#include <algorithm>
#include <numeric>
namespace SOM
{
template<typename T>
static
T
variance(const std::vector<T>& 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<InputVector>& 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<InputVector>& 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
@@ -19,6 +19,9 @@
#pragma once
#include <vector>
#include <ostream>
#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
@@ -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<Coords>
Network::classify(const InputVector& data, std::size_t size) const
{
struct Entry
{
Coords coords;
InputVector refVector;
};
std::vector<Entry> 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<Coords> 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)
{
@@ -61,6 +61,9 @@ class Network
// data must be normalized
Coords classify(const InputVector& data) const;
// ordered from closest to farthest
std::vector<Coords> 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);
+1 -1
View File
@@ -196,7 +196,7 @@ Handler::createConnectionPool(boost::filesystem::path p)
auto connection = std::make_unique<Wt::Dbo::backend::Sqlite3>(p.string());
connection->executeSql("pragma journal_mode=WAL");
connection->setProperty("show-queries", "true");
// connection->setProperty("show-queries", "true");
auto pool = std::make_unique<Wt::Dbo::FixedSqlConnectionPool>(std::move(connection), 1);
pool->setTimeout(std::chrono::seconds(10));
+1 -1
View File
@@ -1,2 +1,2 @@
SUBDIRS = metadata classifier
SUBDIRS = metadata clusterer
-44
View File
@@ -1,44 +0,0 @@
#include <stdlib.h>
#include <stdexcept>
#include <iostream>
#include <string>
#include "classifier/SOM.hpp"
#include "classifier/DataNormalizer.hpp"
#include "classifier/Clusterer.hpp"
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cerr << "Usage: <file>" << std::endl;
return EXIT_FAILURE;
}
auto iterationCount = std::stoul(argv[1]);
std::vector< std::pair<std::vector<SOM::InputVector::value_type>, 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<std::string> 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;
}
-11
View File
@@ -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
+209
View File
@@ -0,0 +1,209 @@
#include <stdlib.h>
#include <stdexcept>
#include <iostream>
#include <string>
#include <boost/filesystem.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <curl/curl.h>
#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<std::string> 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<std::ofstream*>(ctx);
ofs.write(reinterpret_cast<char*>(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<double> getFeatures(const std::string& mbid)
{
std::vector<double> 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<double>(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<std::pair<std::vector<double>, 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<std::vector<double>, 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<Database::IdType> 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;
}
+21
View File
@@ -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