WIP, reworking features

This commit is contained in:
emeric
2019-11-08 13:34:07 +01:00
parent f3c02a84ba
commit 07eae9cf96
18 changed files with 328 additions and 339 deletions
+2
View File
@@ -69,6 +69,8 @@ lms_SOURCES = \
$(srcdir)/similarity/features/AcousticBrainzUtils.hpp \
$(srcdir)/similarity/features/SimilarityFeaturesCache.cpp \
$(srcdir)/similarity/features/SimilarityFeaturesCache.hpp \
$(srcdir)/similarity/features/SimilarityFeaturesDefs.cpp \
$(srcdir)/similarity/features/SimilarityFeaturesDefs.hpp \
$(srcdir)/similarity/features/SimilarityFeaturesScannerAddon.cpp \
$(srcdir)/similarity/features/SimilarityFeaturesScannerAddon.hpp \
$(srcdir)/similarity/features/SimilarityFeaturesSearcher.cpp \
+16 -24
View File
@@ -27,38 +27,30 @@
namespace Database {
struct TrackFeatureInfo
static const std::map<std::string, double> defaultFeatures =
{
std::string name;
std::size_t nbDimensions;
double weight;
{ "lowlevel.spectral_contrast_coeffs.median", 1. },
{ "lowlevel.erbbands.median", 1. },
{ "tonal.hpcp.median", 1. },
{ "lowlevel.melbands.median", 1. },
{ "lowlevel.barkbands.median", 1. },
{ "lowlevel.mfcc.mean", 1. },
{ "lowlevel.gfcc.mean", 1. },
};
static const std::vector<TrackFeatureInfo> defaultFeatures =
{
{ "lowlevel.spectral_contrast_coeffs.median", 6, 1. },
{ "lowlevel.erbbands.median", 40, 1. },
{ "tonal.hpcp.median", 36, 1. },
{ "lowlevel.melbands.median", 40, 1. },
{ "lowlevel.barkbands.median", 27, 1. },
{ "lowlevel.mfcc.mean", 13, 1. },
{ "lowlevel.gfcc.mean", 13, 1. },
};
SimilaritySettingsFeature::SimilaritySettingsFeature(Wt::Dbo::ptr<SimilaritySettings> settings, const std::string& name, std::size_t nbDimensions, double weight)
: _name(name),
_nbDimensions(nbDimensions),
_weight(weight),
_settings(settings)
SimilaritySettingsFeature::SimilaritySettingsFeature(Wt::Dbo::ptr<SimilaritySettings> settings, const std::string& name, double weight)
: _name {name},
_weight {weight},
_settings {settings}
{
}
SimilaritySettingsFeature::pointer
SimilaritySettingsFeature::create(Session& session, Wt::Dbo::ptr<SimilaritySettings> settings, const std::string& name, std::size_t nbDimensions, double weight)
SimilaritySettingsFeature::create(Session& session, Wt::Dbo::ptr<SimilaritySettings> settings, const std::string& name, double weight)
{
session.checkUniqueLocked();
SimilaritySettingsFeature::pointer res {session.getDboSession().add(std::make_unique<SimilaritySettingsFeature>(settings, name, nbDimensions, weight))};
SimilaritySettingsFeature::pointer res {session.getDboSession().add(std::make_unique<SimilaritySettingsFeature>(settings, name, weight))};
session.getDboSession().flush();
return res;
@@ -74,8 +66,8 @@ SimilaritySettings::init(Session& session)
return;
settings = session.getDboSession().add(std::make_unique<SimilaritySettings>());
for (const auto& feature : defaultFeatures)
SimilaritySettingsFeature::create(session, settings, feature.name, feature.nbDimensions, feature.weight);
for (const auto& [feature, weight] : defaultFeatures)
SimilaritySettingsFeature::create(session, settings, feature, weight);
}
+5 -8
View File
@@ -32,19 +32,17 @@ class SimilaritySettingsFeature : public Wt::Dbo::Dbo<SimilaritySettingsFeature
using pointer = Wt::Dbo::ptr<SimilaritySettingsFeature>;
SimilaritySettingsFeature() = default;
SimilaritySettingsFeature(Wt::Dbo::ptr<SimilaritySettings> settings, const std::string& name, std::size_t nbDimensions, double weight);
SimilaritySettingsFeature(Wt::Dbo::ptr<SimilaritySettings> settings, const std::string& name, double weight);
static pointer create(Session& session, Wt::Dbo::ptr<SimilaritySettings> settings, const std::string& name, std::size_t nbDimensions, double weight = 1);
static pointer create(Session& session, Wt::Dbo::ptr<SimilaritySettings> settings, const std::string& name, double weight = 1);
const std::string& getName() const { return _name; } ;
std::size_t getNbDimensions() const { return static_cast<std::size_t>(_nbDimensions); }
double getWeight() const { return _weight; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::field(a, _nbDimensions, "dimension_count");
Wt::Dbo::field(a, _weight, "weight");
Wt::Dbo::belongsTo(a, _settings, "similarity_settings", Wt::Dbo::OnDeleteCascade);
@@ -52,8 +50,7 @@ class SimilaritySettingsFeature : public Wt::Dbo::Dbo<SimilaritySettingsFeature
private:
std::string _name;
int _nbDimensions;
double _weight;
double _weight {1};
Wt::Dbo::ptr<SimilaritySettings> _settings;
};
@@ -75,8 +72,8 @@ class SimilaritySettings : public Wt::Dbo::Dbo<SimilaritySettings>
static pointer get(Session& session);
// Accessors Read
std::size_t getVersion() const { return _settingsVersion; }
EngineType getEngineType() const { return _engineType; }
std::size_t getVersion() const { return _settingsVersion; }
EngineType getEngineType() const { return _engineType; }
std::vector<Wt::Dbo::ptr<SimilaritySettingsFeature>> getFeatures() const;
// Setters
+16 -22
View File
@@ -41,53 +41,47 @@ TrackFeatures::create(Session& session, Wt::Dbo::ptr<Track> track, const std::st
return session.getDboSession().add(std::make_unique<TrackFeatures>(track, jsonEncodedFeatures));
}
std::vector<double>
TrackFeatures::getFeatures(const std::string& featureNode) const
FeatureValues
TrackFeatures::getFeatureValues(const FeatureName& featureNode) const
{
std::vector<double> res;
std::map<std::string, std::vector<double>> features = { {featureNode, {}} };
if (!getFeatures( features ))
return res;
res = std::move(features[featureNode]);
return res;
FeatureValuesMap featuresValuesMap {getFeatureValuesMap({featureNode})};
return std::move(featuresValuesMap[featureNode]);
}
bool
TrackFeatures::getFeatures(std::map<std::string /*name*/, std::vector<double> /*values*/>& features) const
FeatureValuesMap
TrackFeatures::getFeatureValuesMap(const std::unordered_set<FeatureName>& featureNames) const
{
try
{
std::istringstream iss {_data};
boost::property_tree::ptree root;
std::istringstream iss(_data);
boost::property_tree::read_json(iss, root);
for (auto& featureNode : features)
FeatureValuesMap res;
for (const FeatureName& featureName : featureNames)
{
auto node = root.get_child(featureNode.first);
FeatureValues& featureValues {res[featureName]};
auto node {root.get_child(featureName)};
bool hasChildren = false;
for (const auto& child : node.get_child(""))
{
hasChildren = true;
featureNode.second.push_back(child.second.get_value<double>());
featureValues.push_back(child.second.get_value<double>());
}
if (!hasChildren)
{
featureNode.second.push_back(node.get_value<double>());
}
featureValues.push_back(node.get_value<double>());
}
return true;
return res;
}
catch (boost::property_tree::ptree_error& error)
{
LMS_LOG(SIMILARITY, ERROR) << "Track " << _track.id() << ": ptree exception: " << error.what();
return false;
return {};
}
}
+9 -2
View File
@@ -20,6 +20,9 @@
#pragma once
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <Wt/Dbo/Dbo.h>
@@ -30,6 +33,10 @@ namespace Database {
class Session;
class Track;
using FeatureName = std::string;
using FeatureValues = std::vector<double>;
using FeatureValuesMap = std::unordered_map<FeatureName, FeatureValues>;
class TrackFeatures : public Wt::Dbo::Dbo<TrackFeatures>
{
public:
@@ -42,8 +49,8 @@ class TrackFeatures : public Wt::Dbo::Dbo<TrackFeatures>
// Create utility
static pointer create(Session& session, Wt::Dbo::ptr<Track> track, const std::string& jsonEncodedFeatures);
std::vector<double> getFeatures(const std::string& featureNode) const;
bool getFeatures(std::map<std::string /*featureNode*/, std::vector<double> /*values*/>& featureNodes) const;
FeatureValues getFeatureValues(const FeatureName& feature) const;
FeatureValuesMap getFeatureValuesMap(const std::unordered_set<FeatureName>& featureNames) const;
template<class Action>
void persist(Action& a)
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2019 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 <http://www.gnu.org/licenses/>.
*/
#include "SimilarityFeaturesDefs.hpp"
#include <unordered_map>
#include "utils/Exception.hpp"
namespace Similarity {
static const std::unordered_map<FeatureName, FeatureDef> featureDefinitions
{
{ "lowlevel.spectral_contrast_coeffs.median", {6}},
{ "lowlevel.erbbands.median", {40}},
{ "tonal.hpcp.median", {36}},
{ "lowlevel.melbands.median", {40}},
{ "lowlevel.barkbands.median", {27}},
{ "lowlevel.mfcc.mean", {13}},
{ "lowlevel.gfcc.mean", {13}},
};
FeatureDef
getFeatureDef(const FeatureName& featureName)
{
auto it {featureDefinitions.find(featureName)};
if (it == std::cend(featureDefinitions))
throw LmsException {"Unhandled requested feature '" + featureName + "'"};
return it->second;
}
} // namespace Similarity
@@ -0,0 +1,46 @@
/*
* Copyright (C) 2019 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <string>
#include <unordered_map>
#include <vector>
namespace Similarity {
using FeatureName = std::string;
using FeatureValue = double;
using FeatureValues = std::vector<FeatureValue>;
using FeatureValuesMap = std::unordered_map<FeatureName, FeatureValues>;
struct FeatureDef
{
std::size_t nbDimensions {};
};
FeatureDef getFeatureDef(const FeatureName& featureName);
struct FeatureSettings
{
double weight {};
};
using FeatureSettingsMap = std::unordered_map<FeatureName, FeatureSettings>;
} // namespace Similarity
@@ -29,7 +29,27 @@
namespace Similarity {
namespace {
static
FeatureSettingsMap
getFeatureSettings(Database::Session& session)
{
FeatureSettingsMap res;
auto transaction {session.createSharedTransaction()};
for (const auto& feature : Database::SimilaritySettings::get(session)->getFeatures())
res[feature->getName()] = {feature->getWeight()};
return res;
}
static
bool
hasAtLeastOneTrackWithFeatures(Database::Session& session)
{
auto transaction {session.createSharedTransaction()};
return !Database::Track::getAllIdsWithFeatures(session).empty();
}
struct TrackInfo
{
@@ -37,6 +57,7 @@ struct TrackInfo
std::string mbid;
};
static
std::vector<TrackInfo>
getTracksWithMBIDAndMissingFeatures(Database::Session& dbSession)
{
@@ -51,8 +72,6 @@ getTracksWithMBIDAndMissingFeatures(Database::Session& dbSession)
return res;
}
} // namespace
FeaturesScannerAddon::FeaturesScannerAddon(std::unique_ptr<Database::Session> dbSession)
: _dbSession {std::move(dbSession)}
{
@@ -125,20 +144,16 @@ FeaturesScannerAddon::updateSearcher()
{
LMS_LOG(SIMILARITY, INFO) << "Updating searcher...";
std::vector<Database::IdType> trackIds;
{
auto transaction {_dbSession->createSharedTransaction()};
trackIds = Database::Track::getAllIdsWithFeatures(*_dbSession);
}
if (trackIds.empty())
if (hasAtLeastOneTrackWithFeatures(*_dbSession))
{
LMS_LOG(DBUPDATER, INFO) << "No track suitable for features similarity clustering";
std::atomic_store(&_searcher, std::shared_ptr<FeaturesSearcher>{});
return;
}
auto searcher {std::make_shared<Similarity::FeaturesSearcher>(*_dbSession, [&]() { return _stopRequested; })};
const auto features {getFeatureSettings(*_dbSession)};
auto searcher {std::make_shared<Similarity::FeaturesSearcher>(*_dbSession, features, [&]() { return _stopRequested; })};
if (searcher->isValid())
{
std::atomic_store(&_searcher, searcher);
@@ -20,6 +20,7 @@
#include "SimilarityFeaturesSearcher.hpp"
#include <random>
#include <unordered_map>
#include "database/Artist.hpp"
#include "database/SimilaritySettings.hpp"
@@ -34,71 +35,44 @@
namespace Similarity {
struct FeatureInfo
{
std::size_t nbDimensions;
double weight;
};
using FeatureInfoMap = std::map<std::string, FeatureInfo>;
static
FeatureInfoMap
getFeatureInfoMap(Database::Session& session)
{
auto transaction {session.createSharedTransaction()};
auto settings {Database::SimilaritySettings::get(session)};
std::map<std::string, FeatureInfo> 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
std::size_t
getFeatureInfoMapNbDimensions(const FeatureInfoMap& featureInfoMap)
{
return std::accumulate(featureInfoMap.begin(), featureInfoMap.end(), 0, [](std::size_t sum, auto it) { return sum + it.second.nbDimensions; });
}
static
std::optional<SOM::InputVector>
getInputVectorFromTrack(Database::Session& session, Database::IdType trackId, const FeatureInfoMap& featuresInfo, std::size_t nbDimensions)
getInputVectorFromTrack(Database::Session& session, Database::IdType trackId, const std::unordered_set<FeatureName>& featureNames, std::size_t nbDimensions)
{
std::optional<SOM::InputVector> res {SOM::InputVector {nbDimensions}};
FeatureValuesMap featureValuesMap;
std::map<std::string, std::vector<double>> features;
for (auto itFeatureInfo : featuresInfo)
features[itFeatureInfo.first] = {};
{
auto transaction {session.createSharedTransaction()};
auto transaction {session.createSharedTransaction()};
Database::Track::pointer track {Database::Track::getById(session, trackId)};
if (!track)
return std::nullopt;
Database::Track::pointer track {Database::Track::getById(session, trackId)};
if (!track)
return res;
if (!track->getTrackFeatures()->getFeatures(features))
return res;
featureValuesMap = track->getTrackFeatures()->getFeatureValuesMap(featureNames);
if (featureValuesMap.empty())
return std::nullopt;
}
std::size_t i {};
for (const auto& feature : features)
std::optional<SOM::InputVector> res {SOM::InputVector {nbDimensions}};
for (const auto& featureName : featureNames)
{
// Check dimensions for each feature
auto it {featuresInfo.find(feature.first)};
if (it == featuresInfo.end() || it->second.nbDimensions != feature.second.size())
const auto it {featureValuesMap.find(featureName)};
if (it == std::cend(featureValuesMap))
{
LMS_LOG(SIMILARITY, WARNING) << "Dimension mismatch for feature '" << feature.first << "'. Expected " << it->second.nbDimensions << ", got " << feature.second.size();
LMS_LOG(SIMILARITY, WARNING) << "Cannot find feature '" << featureName << "' for track id'" << trackId << "'";
res.reset();
break;
}
for (double val : feature.second)
if (it->second.size() != getFeatureDef(featureName).nbDimensions)
{
LMS_LOG(SIMILARITY, WARNING) << "Dimension mismatch for feature '" << featureName << "'. Expected " << getFeatureDef(featureName).nbDimensions << ", got " << it->second.size() << ", trackId = " << trackId;
res.reset();
break;
}
for (double val : it->second)
(*res)[i++] = val;
}
@@ -107,25 +81,35 @@ getInputVectorFromTrack(Database::Session& session, Database::IdType trackId, co
static
SOM::InputVector
getInputVectorWeights(const FeatureInfoMap& featuresInfo, std::size_t nbDimensions)
getInputVectorWeights(const FeatureSettingsMap& featureSettingsMap, std::size_t nbDimensions)
{
SOM::InputVector weights {nbDimensions};
std::size_t index {};
for (const auto& featureInfo : featuresInfo)
for (const auto& [featureName, featureSettings] : featureSettingsMap)
{
for (std::size_t i {}; i < featureInfo.second.nbDimensions; ++i)
weights[index++] = (1. / featureInfo.second.nbDimensions * featureInfo.second.weight);
const std::size_t featureNbDimensions {getFeatureDef(featureName).nbDimensions};
for (std::size_t i {}; i < featureNbDimensions; ++i)
weights[index++] = (1. / featureNbDimensions * featureSettings.weight);
}
assert(index == nbDimensions);
return weights;
}
FeaturesSearcher::FeaturesSearcher(Database::Session& session, std::function<bool()> stopRequested)
FeaturesSearcher::FeaturesSearcher(Database::Session& session,
const FeatureSettingsMap& featureSettingsMap,
StopRequestedFunction stopRequested)
{
LMS_LOG(SIMILARITY, INFO) << "Constructing features searcher...";
const FeatureInfoMap featuresInfo {getFeatureInfoMap(session)};
const std::size_t nbDimensions {getFeatureInfoMapNbDimensions(featuresInfo)};
std::unordered_set<FeatureName> featureNames;
std::transform(std::cbegin(featureSettingsMap), std::cend(featureSettingsMap), std::inserter(featureNames, std::begin(featureNames)),
[](const auto& itFeatureSetting) { return itFeatureSetting.first; });
const std::size_t nbDimensions {std::accumulate(std::cbegin(featureNames), std::cend(featureNames), std::size_t {0},
[](std::size_t sum, const FeatureName& featureName) { return sum + getFeatureDef(featureName).nbDimensions; })};
LMS_LOG(SIMILARITY, DEBUG) << "Features dimension = " << nbDimensions;
@@ -147,10 +131,10 @@ FeaturesSearcher::FeaturesSearcher(Database::Session& session, std::function<boo
LMS_LOG(SIMILARITY, DEBUG) << "Extracting features...";
for (Database::IdType trackId : trackIds)
{
if (stopRequested())
if (stopRequested && stopRequested())
return;
std::optional<SOM::InputVector> inputVector {getInputVectorFromTrack(session, trackId, featuresInfo, nbDimensions)};
std::optional<SOM::InputVector> inputVector {getInputVectorFromTrack(session, trackId, featureNames, nbDimensions)};
if (!inputVector)
continue;
@@ -177,7 +161,7 @@ FeaturesSearcher::FeaturesSearcher(Database::Session& session, std::function<boo
SOM::Network network {size, size, nbDimensions};
SOM::InputVector weights {getInputVectorWeights(featuresInfo, nbDimensions)};
SOM::InputVector weights {getInputVectorWeights(featureSettingsMap, nbDimensions)};
network.setDataWeights(weights);
auto progressIndicator{[](const auto& iter)
@@ -189,14 +173,14 @@ FeaturesSearcher::FeaturesSearcher(Database::Session& session, std::function<boo
network.train(samples, 10, progressIndicator, stopRequested);
LMS_LOG(SIMILARITY, DEBUG) << "Training network DONE";
if (stopRequested())
if (stopRequested && stopRequested())
return;
LMS_LOG(SIMILARITY, DEBUG) << "Classifying tracks...";
std::map<Database::IdType, std::set<SOM::Position>> trackPositions;
for (std::size_t i {}; i < samples.size(); ++i)
{
if (stopRequested())
if (stopRequested && stopRequested())
return;
const SOM::Position position {network.getClosestRefVectorPosition(samples[i])};
@@ -211,7 +195,7 @@ FeaturesSearcher::FeaturesSearcher(Database::Session& session, std::function<boo
LMS_LOG(SIMILARITY, INFO) << "Successfully constructed features searcher";
}
FeaturesSearcher::FeaturesSearcher(Database::Session& session, FeaturesCache cache, std::function<bool()> stopRequested)
FeaturesSearcher::FeaturesSearcher(Database::Session& session, FeaturesCache cache, StopRequestedFunction stopRequested)
{
LMS_LOG(SIMILARITY, INFO) << "Constructing features searcher from cache...";
@@ -341,7 +325,7 @@ FeaturesSearcher::init(Database::Session& session,
for (auto itTrackCoord : tracksPosition)
{
if (stopRequested())
if (stopRequested && stopRequested())
return;
auto transaction {session.createSharedTransaction()};
@@ -21,11 +21,13 @@
#include <map>
#include <set>
#include <string>
#include "database/Types.hpp"
#include "som/DataNormalizer.hpp"
#include "som/Network.hpp"
#include "SimilarityFeaturesCache.hpp"
#include "SimilarityFeaturesDefs.hpp"
namespace Database
{
@@ -34,15 +36,19 @@ namespace Database
namespace Similarity {
using FeatureWeight = double;
class FeaturesSearcher
{
public:
using StopRequestedFunction = std::function<bool()>; // return true if stop requested
// Use cache
FeaturesSearcher(Database::Session& session, FeaturesCache cache, std::function<bool()> stopRequested);
FeaturesSearcher(Database::Session& session, FeaturesCache cache, StopRequestedFunction stopRequested);
// Use training (may be very slow)
FeaturesSearcher(Database::Session& session, std::function<bool()> stopRequested);
FeaturesSearcher(Database::Session& session, const FeatureSettingsMap& featuresSettingsMap, StopRequestedFunction stopRequested = {});
bool isValid() const;
@@ -65,7 +71,7 @@ class FeaturesSearcher
void init(Database::Session& session,
SOM::Network network,
ObjectPositions tracksPosition,
std::function<bool()> stopRequested);
StopRequestedFunction stopRequested);
std::vector<Database::IdType> getSimilarObjects(const std::set<Database::IdType>& ids,
const SOM::Matrix<std::set<Database::IdType>>& objectsMap,
+2 -2
View File
@@ -19,7 +19,7 @@
#include "Logger.hpp"
std::string getModuleName(Module mod)
const char* getModuleName(Module mod)
{
switch (mod)
{
@@ -41,7 +41,7 @@ std::string getModuleName(Module mod)
return "";
}
std::string getSeverityName(Severity sev)
const char* getSeverityName(Severity sev)
{
switch (sev)
{
+8 -2
View File
@@ -51,8 +51,14 @@ enum class Module
UI,
};
std::string getModuleName(Module mod);
std::string getSeverityName(Severity sev);
const char* getModuleName(Module mod);
const char* getSeverityName(Severity sev);
TODO class logger
TODO class log entry
TODO configure logger to redirect to either Wt's logger or to a ostream
#define LMS_LOG(module, level) Wt::log(getSeverityName(Severity::level)) << Wt::WLogger::sep << "[" << getModuleName(Module::module) << "]" << Wt::WLogger::sep