diff --git a/src/libs/recommendation/CMakeLists.txt b/src/libs/recommendation/CMakeLists.txt
index 26e29757..b7a52f12 100644
--- a/src/libs/recommendation/CMakeLists.txt
+++ b/src/libs/recommendation/CMakeLists.txt
@@ -1,8 +1,10 @@
add_library(lmsrecommendation SHARED
impl/clusters/ClustersClassifier.cpp
+ impl/features/FeaturesClassifierCache.cpp
+ impl/features/FeaturesClassifier.cpp
+ impl/features/FeaturesDefs.cpp
impl/Engine.cpp
- impl/ClassifierCreator.cpp
)
target_include_directories(lmsrecommendation INTERFACE
diff --git a/src/libs/recommendation/impl/ClassifierCreator.cpp b/src/libs/recommendation/impl/ClassifierCreator.cpp
deleted file mode 100644
index 90dd651b..00000000
--- a/src/libs/recommendation/impl/ClassifierCreator.cpp
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2020 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 "recommendation/FeaturesClassifierCreator.hpp"
-#include "recommendation/IClassifier.hpp"
-
-namespace Recommendation
-{
-
- std::unique_ptr createFeaturesClassifier()
- {
- return {};
- }
-
-
-}
-
diff --git a/src/libs/recommendation/impl/Engine.cpp b/src/libs/recommendation/impl/Engine.cpp
index 2a4aa772..4b4a6b67 100644
--- a/src/libs/recommendation/impl/Engine.cpp
+++ b/src/libs/recommendation/impl/Engine.cpp
@@ -24,6 +24,7 @@
#include "database/ScanSettings.hpp"
#include "database/TrackList.hpp"
+#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
namespace Recommendation {
@@ -56,6 +57,8 @@ Engine::stop()
assert(_running);
_running = false;
+ cancelPendingClassifiers();
+
_ioService.stop();
}
@@ -73,32 +76,18 @@ Engine::requestReload()
std::vector
Engine::getSimilarTracksFromTrackList(Database::Session& session, Database::IdType trackListId, std::size_t maxCount)
{
- const std::unordered_set trackIds {[&]() -> std::unordered_set
- {
- auto transaction {session.createSharedTransaction()};
-
- Database::TrackList::pointer trackList {Database::TrackList::getById(session, trackListId)};
- if (trackList)
- {
- const std::vector orderedTrackIds {trackList->getTrackIds()};
- return std::unordered_set {std::cbegin(orderedTrackIds), std::cend(orderedTrackIds)};
- }
-
- return {};
- }()};
-
- if (trackIds.empty())
- return {};
-
std::shared_lock lock {_classifiersMutex};
+ std::vector res;
+
for (const auto& [priority, classifier] : _classifiers)
{
- if (std::any_of(std::cbegin(trackIds), std::cend(trackIds), [&](Database::IdType trackId) { return classifier->isTrackClassified(trackId); } ))
- return classifier->getSimilarTracksFromTrackList(session, trackListId, maxCount);
+ res = classifier->getSimilarTracksFromTrackList(session, trackListId, maxCount);
+ if (!res.empty())
+ break;
}
- return {};
+ return res;
}
std::vector
@@ -106,13 +95,16 @@ Engine::getSimilarTracks(Database::Session& dbSession, const std::unordered_set<
{
std::shared_lock lock {_classifiersMutex};
+ std::vector res;
+
for (const auto& [priority, classifier] : _classifiers)
{
- if (std::any_of(std::cbegin(trackIds), std::cend(trackIds), [&](Database::IdType trackId) { return classifier->isTrackClassified(trackId); } ))
- return classifier->getSimilarTracks(dbSession, trackIds, maxCount);
+ res = classifier->getSimilarTracks(dbSession, trackIds, maxCount);
+ if (!res.empty())
+ break;
}
- return {};
+ return res;
}
std::vector
@@ -120,13 +112,16 @@ Engine::getSimilarReleases(Database::Session& dbSession, Database::IdType releas
{
std::shared_lock lock {_classifiersMutex};
+ std::vector res;
+
for (const auto& [priority, classifier] : _classifiers)
{
- if (classifier->isReleaseClassified(releaseId))
- return classifier->getSimilarReleases(dbSession, releaseId, maxCount);
+ res = classifier->getSimilarReleases(dbSession, releaseId, maxCount);
+ if (!res.empty())
+ break;
}
- return {};
+ return res;
}
std::vector
@@ -134,13 +129,16 @@ Engine::getSimilarArtists(Database::Session& dbSession, Database::IdType artistI
{
std::shared_lock lock {_classifiersMutex};
+ std::vector res;
+
for (const auto& [priority, classifier] : _classifiers)
{
- if (classifier->isArtistClassified(artistId))
- return classifier->getSimilarArtists(dbSession, artistId, maxCount);
+ res = classifier->getSimilarArtists(dbSession, artistId, maxCount);
+ if (!res.empty())
+ return res;
}
- return {};
+ return res;
}
void
@@ -159,14 +157,35 @@ Engine::reload()
std::map> newClassifiers;
+ // TODO RAII this
+ auto addClassifier = [&](ClassifierPriority prio, std::unique_ptr classifier)
+ {
+ try
+ {
+ addPendingClassifier(*classifier.get());
+ bool res {classifier->init(_dbSession)};
+ removePendingClassifier(*classifier.get());
+
+ if (res)
+ newClassifiers.emplace(prio, std::move(classifier));
+
+ return res;
+ }
+ catch (LmsException& e)
+ {
+ removePendingClassifier(*classifier.get());
+ throw;
+ }
+ };
+
switch (engineType)
{
case ScanSettings::RecommendationEngineType::Features:
-// newClassifiers.emplace_back(0, createFeaturesClassifier()); // higher priority
-// [[fallthrough]];
+ addClassifier(0, createFeaturesClassifier()); // higher priority
+ [[fallthrough]];
case ScanSettings::RecommendationEngineType::Clusters:
- newClassifiers.emplace(1, createClustersClassifier(_dbSession)); // lower priority
+ addClassifier(1, createClustersClassifier()); // lower priority
break;
}
@@ -180,19 +199,29 @@ Engine::reload()
_sigReloaded.emit();
}
-
void
-Engine::clearClassifiers()
+Engine::cancelPendingClassifiers()
{
+ std::unique_lock lock {_classifiersMutex};
+
+ for (IClassifier* classifier : _pendingClassifiers)
+ classifier->requestCancelInit();
}
void
-Engine::addClassifier(std::unique_ptr classifier, unsigned priority)
+Engine::addPendingClassifier(IClassifier& classifier)
{
- std::unique_lock lock {_classifiersMutex};
+ std::unique_lock lock {_classifiersMutex};
- _classifiers.emplace(priority, std::move(classifier));
+ _pendingClassifiers.insert(&classifier);
}
+void
+Engine::removePendingClassifier(IClassifier& classifier)
+{
+ std::unique_lock lock {_classifiersMutex};
+
+ _pendingClassifiers.erase(&classifier);
+}
} // ns Similarity
diff --git a/src/libs/recommendation/impl/Engine.hpp b/src/libs/recommendation/impl/Engine.hpp
index 9fde430e..17208b77 100644
--- a/src/libs/recommendation/impl/Engine.hpp
+++ b/src/libs/recommendation/impl/Engine.hpp
@@ -52,8 +52,9 @@ namespace Recommendation
void reload();
- void clearClassifiers();
- void addClassifier(std::unique_ptr classifier, unsigned priority);
+ void cancelPendingClassifiers();
+ void addPendingClassifier(IClassifier& classifier);
+ void removePendingClassifier(IClassifier& classifier);
bool _running {};
Wt::WIOService _ioService;
@@ -62,6 +63,7 @@ namespace Recommendation
std::shared_mutex _classifiersMutex;
std::map> _classifiers;
+ std::unordered_set _pendingClassifiers;
};
} // ns Recommendation
diff --git a/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp b/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp
index f0aee487..b433d833 100644
--- a/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp
+++ b/src/libs/recommendation/impl/clusters/ClustersClassifier.cpp
@@ -28,56 +28,11 @@
namespace Recommendation {
-std::unique_ptr createClustersClassifier(Database::Session& session)
+std::unique_ptr createClustersClassifier()
{
- return std::make_unique(session);
+ return std::make_unique();
}
-ClusterClassifier::ClusterClassifier(Database::Session& session)
-{
- classify(session);
-}
-
-void
-ClusterClassifier::classify(Database::Session& session)
-{
- auto transaction {session.createSharedTransaction()};
-
- {
- std::vector trackIds {Database::Track::getAllIdsWithClusters(session)};
- _classifiedTracks = std::unordered_set(std::cbegin(trackIds), std::cend(trackIds));
- }
-
- {
- std::vector releaseIds {Database::Release::getAllIdsWithClusters(session)};
- _classifiedReleases = std::unordered_set(std::cbegin(releaseIds), std::cend(releaseIds));
- }
-
- {
- std::vector artistIds {Database::Artist::getAllIdsWithClusters(session)};
- _classifiedArtists = std::unordered_set(std::cbegin(artistIds), std::cend(artistIds));
- }
-}
-
-bool
-ClusterClassifier::isTrackClassified(Database::IdType trackId) const
-{
- return _classifiedTracks.find(trackId) != std::cend(_classifiedTracks);
-}
-
-bool
-ClusterClassifier::isReleaseClassified(Database::IdType releaseId) const
-{
- return _classifiedReleases.find(releaseId) != std::cend(_classifiedReleases);
-}
-
-bool
-ClusterClassifier::isArtistClassified(Database::IdType artistId) const
-{
- return _classifiedArtists.find(artistId) != std::cend(_classifiedArtists);
-}
-
-
std::vector
ClusterClassifier::getSimilarTracks(Database::Session& dbSession, const std::unordered_set& trackIds, std::size_t maxCount) const
{
diff --git a/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp b/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp
index 2f44205e..469a6ee8 100644
--- a/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp
+++ b/src/libs/recommendation/impl/clusters/ClustersClassifier.hpp
@@ -28,7 +28,7 @@ namespace Recommendation
class ClusterClassifier : public IClassifier
{
public:
- ClusterClassifier(Database::Session& session);
+ ClusterClassifier() = default;
ClusterClassifier(const ClusterClassifier&) = delete;
ClusterClassifier(ClusterClassifier&&) = delete;
ClusterClassifier& operator=(const ClusterClassifier&) = delete;
@@ -36,20 +36,16 @@ namespace Recommendation
private:
- bool isTrackClassified(Database::IdType trackId) const override;
- bool isReleaseClassified(Database::IdType releaseId) const override;
- bool isArtistClassified(Database::IdType artistId) const override;
+ std::string_view getName() const { return "Clusters"; }
+
+ bool init(Database::Session&) override {return true;}
+ void requestCancelInit() override {}
std::vector getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const override;
std::vector getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) const override;
std::vector getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const override;
std::vector getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const override;
- void classify(Database::Session& session);
-
- std::unordered_set _classifiedArtists;
- std::unordered_set _classifiedReleases;
- std::unordered_set _classifiedTracks;
};
} // namespace Recommendation
diff --git a/src/libs/recommendation/impl/features/FeaturesClassifier.cpp b/src/libs/recommendation/impl/features/FeaturesClassifier.cpp
new file mode 100644
index 00000000..f4f4ad90
--- /dev/null
+++ b/src/libs/recommendation/impl/features/FeaturesClassifier.cpp
@@ -0,0 +1,445 @@
+/*
+ * 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 "FeaturesClassifier.hpp"
+
+#include
+
+#include "database/Artist.hpp"
+#include "database/Release.hpp"
+#include "database/Session.hpp"
+#include "database/Track.hpp"
+#include "database/TrackFeatures.hpp"
+#include "database/TrackList.hpp"
+#include "som/DataNormalizer.hpp"
+#include "utils/Logger.hpp"
+#include "utils/Random.hpp"
+
+
+namespace Recommendation {
+
+std::unique_ptr createFeaturesClassifier()
+{
+ return std::make_unique();
+}
+
+const FeatureSettingsMap&
+FeaturesClassifier::getDefaultTrainFeatureSettings()
+{
+ static const FeatureSettingsMap defaultTrainFeatureSettings
+ {
+ { "lowlevel.spectral_energyband_high.mean", {1}},
+ { "lowlevel.spectral_rolloff.median", {1}},
+ { "lowlevel.spectral_contrast_valleys.var", {1}},
+ { "lowlevel.erbbands.mean", {1}},
+ { "lowlevel.gfcc.mean", {1}},
+ };
+
+ return defaultTrainFeatureSettings;
+}
+
+static
+std::optional
+getTrackFeatureValues(FeaturesClassifier::FeaturesFetchFunc func, Database::IdType trackId, const std::unordered_set& featureNames)
+{
+ return func(trackId, featureNames);
+}
+
+static
+std::optional
+getTrackFeatureValuesFromDb(Database::Session& session, Database::IdType trackId, const std::unordered_set& featureNames)
+{
+ auto func = [&](Database::IdType trackId, const std::unordered_set& featureNames)
+ {
+ std::optional res;
+
+ auto transaction {session.createSharedTransaction()};
+
+ Database::Track::pointer track {Database::Track::getById(session, trackId)};
+ if (!track)
+ return res;
+
+ res = track->getTrackFeatures()->getFeatureValuesMap(featureNames);
+ if (res->empty())
+ res.reset();
+
+ return res;
+ };
+
+ return getTrackFeatureValues(func, trackId, featureNames);
+}
+
+static
+std::optional
+convertFeatureValuesMapToInputVector(const FeatureValuesMap& featureValuesMap, std::size_t nbDimensions)
+{
+ std::size_t i {};
+ std::optional res {SOM::InputVector {nbDimensions}};
+ for (const auto& [featureName, values] : featureValuesMap)
+ {
+ if (values.size() != getFeatureDef(featureName).nbDimensions)
+ {
+ LMS_LOG(RECOMMENDATION, WARNING) << "Dimension mismatch for feature '" << featureName << "'. Expected " << getFeatureDef(featureName).nbDimensions << ", got " << values.size();
+ res.reset();
+ break;
+ }
+
+ for (double val : values)
+ (*res)[i++] = val;
+ }
+
+ return res;
+}
+
+static
+SOM::InputVector
+getInputVectorWeights(const FeatureSettingsMap& featureSettingsMap, std::size_t nbDimensions)
+{
+ SOM::InputVector weights {nbDimensions};
+ std::size_t index {};
+ for (const auto& [featureName, featureSettings] : featureSettingsMap)
+ {
+ 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;
+}
+
+bool
+FeaturesClassifier::initFromTraining(Database::Session& session, const TrainSettings& trainSettings)
+{
+ LMS_LOG(RECOMMENDATION, INFO) << "Constructing features classifier...";
+
+ std::unordered_set featureNames;
+ std::transform(std::cbegin(trainSettings.featureSettingsMap), std::cend(trainSettings.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(RECOMMENDATION, DEBUG) << "Features dimension = " << nbDimensions;
+
+ std::vector trackIds;
+ {
+ auto transaction {session.createSharedTransaction()};
+
+ LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Tracks with features...";
+ trackIds = Database::Track::getAllIdsWithFeatures(session);
+ LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Tracks with features DONE (found " << trackIds.size() << " tracks)";
+ }
+
+ std::vector samples;
+ std::vector samplesTrackIds;
+
+ samples.reserve(trackIds.size());
+ samplesTrackIds.reserve(trackIds.size());
+
+ LMS_LOG(RECOMMENDATION, DEBUG) << "Extracting features...";
+ for (Database::IdType trackId : trackIds)
+ {
+ if (_initCancelled)
+ return false;
+
+ std::optional featureValuesMap;
+
+ if (_featuresFetchFunc)
+ featureValuesMap = getTrackFeatureValues(_featuresFetchFunc, trackId, featureNames);
+ else
+ featureValuesMap = getTrackFeatureValuesFromDb(session, trackId, featureNames);
+
+ if (!featureValuesMap)
+ continue;
+
+ std::optional inputVector {convertFeatureValuesMapToInputVector(*featureValuesMap, nbDimensions)};
+ if (!inputVector)
+ continue;
+
+ samples.emplace_back(std::move(*inputVector));
+ samplesTrackIds.emplace_back(trackId);
+ }
+ LMS_LOG(RECOMMENDATION, DEBUG) << "Extracting features DONE";
+
+ if (samples.empty())
+ {
+ LMS_LOG(RECOMMENDATION, INFO) << "Nothing to classify!";
+ return false;
+ }
+
+ LMS_LOG(RECOMMENDATION, DEBUG) << "Normalizing data...";
+ SOM::DataNormalizer dataNormalizer {nbDimensions};
+
+ dataNormalizer.computeNormalizationFactors(samples);
+ for (auto& sample : samples)
+ dataNormalizer.normalizeData(sample);
+
+ const SOM::Coordinate size {static_cast(std::sqrt(samples.size() / trainSettings.sampleCountPerNeuron))};
+ LMS_LOG(RECOMMENDATION, INFO) << "Found " << samples.size() << " tracks, constructing a " << size << "*" << size << " network";
+
+ SOM::Network network {size, size, nbDimensions};
+
+ SOM::InputVector weights {getInputVectorWeights(trainSettings.featureSettingsMap, nbDimensions)};
+ network.setDataWeights(weights);
+
+ auto progressIndicator{[](const auto& iter)
+ {
+ LMS_LOG(RECOMMENDATION, DEBUG) << "Current pass = " << iter.idIteration << " / " << iter.iterationCount;
+ }};
+
+ LMS_LOG(RECOMMENDATION, DEBUG) << "Training network...";
+ network.train(samples, trainSettings.iterationCount, progressIndicator);
+ LMS_LOG(RECOMMENDATION, DEBUG) << "Training network DONE";
+
+ if (_initCancelled)
+ return false;
+
+ LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks...";
+ ObjectPositions trackPositions;
+ for (std::size_t i {}; i < samples.size(); ++i)
+ {
+ if (_initCancelled)
+ return false;
+
+ const SOM::Position position {network.getClosestRefVectorPosition(samples[i])};
+
+ trackPositions[samplesTrackIds[i]].insert(position);
+ }
+
+ LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks DONE";
+
+ return init(session, std::move(network), std::move(trackPositions));
+}
+
+bool
+FeaturesClassifier::initFromCache(Database::Session& session, const FeaturesClassifierCache& cache)
+{
+ LMS_LOG(RECOMMENDATION, INFO) << "Constructing features classifier from cache...";
+
+ return init(session, std::move(cache._network), cache._trackPositions);
+}
+
+std::vector
+FeaturesClassifier::getSimilarTracksFromTrackList(Database::Session& session, Database::IdType trackListId, std::size_t maxCount) const
+{
+ const std::unordered_set trackIds {[&]() -> std::unordered_set
+ {
+ auto transaction {session.createSharedTransaction()};
+
+ const Database::TrackList::pointer trackList {Database::TrackList::getById(session, trackListId)};
+ if (trackList)
+ {
+ const std::vector orderedTrackIds {trackList->getTrackIds()};
+ return std::unordered_set {std::cbegin(orderedTrackIds), std::cend(orderedTrackIds)};
+ }
+
+ return {};
+ }()};
+
+ return getSimilarTracks(session, trackIds, maxCount);
+}
+
+std::vector
+FeaturesClassifier::getSimilarTracks(Database::Session&, const std::unordered_set& tracksIds, std::size_t maxCount) const
+{
+ return getSimilarObjects(tracksIds, _tracksMap, _trackPositions, maxCount);
+}
+
+std::vector
+FeaturesClassifier::getSimilarReleases(Database::Session&, Database::IdType releaseId, std::size_t maxCount) const
+{
+ return getSimilarObjects({releaseId}, _releasesMap, _releasePositions, maxCount);
+}
+
+std::vector
+FeaturesClassifier::getSimilarArtists(Database::Session&, Database::IdType artistId, std::size_t maxCount) const
+{
+ return getSimilarObjects({artistId}, _artistsMap, _artistPositions, maxCount);
+}
+
+FeaturesClassifierCache
+FeaturesClassifier::toCache() const
+{
+ return FeaturesClassifierCache {*_network, _trackPositions};
+}
+
+bool
+FeaturesClassifier::init(Database::Session& session)
+{
+ std::optional cache {FeaturesClassifierCache::read()};
+ if (cache)
+ return initFromCache(session, *cache);
+
+ TrainSettings trainSettings;
+ trainSettings.featureSettingsMap = getDefaultTrainFeatureSettings();
+
+ bool res {initFromTraining(session, trainSettings)};
+ if (res)
+ toCache().write();
+
+ return res;
+}
+
+void
+FeaturesClassifier::requestCancelInit()
+{
+ LMS_LOG(RECOMMENDATION, DEBUG) << "Requesting init cancellation";
+ _initCancelled = true;
+}
+
+bool
+FeaturesClassifier::init(Database::Session& session,
+ SOM::Network network,
+ const ObjectPositions& tracksPosition)
+{
+ _networkRefVectorsDistanceMedian = network.computeRefVectorsDistanceMedian();
+ LMS_LOG(RECOMMENDATION, DEBUG) << "Median distance betweend ref vectors = " << _networkRefVectorsDistanceMedian;
+
+ const SOM::Coordinate width {network.getWidth()};
+ const SOM::Coordinate height {network.getHeight()};
+
+ _artistsMap = MatrixOfObjects {width, height};
+ _releasesMap = MatrixOfObjects {width, height};
+ _tracksMap = MatrixOfObjects {width, height};
+
+ LMS_LOG(RECOMMENDATION, DEBUG) << "Constructing maps...";
+
+ for (auto itTrackCoord : tracksPosition)
+ {
+ if (_initCancelled)
+ return false;
+
+ auto transaction {session.createSharedTransaction()};
+
+ Database::IdType trackId {itTrackCoord.first};
+ const std::unordered_set& positionSet {itTrackCoord.second};
+
+ const Database::Track::pointer track {Database::Track::getById(session, trackId)};
+ if (!track)
+ continue;
+
+ for (const SOM::Position& position : positionSet)
+ {
+ _tracksMap[position].insert(trackId);
+ _trackPositions[trackId].insert(position);
+
+ if (track->getRelease())
+ {
+ _releasePositions[track->getRelease().id()].insert(position);
+ _releasesMap[position].insert(track->getRelease().id());
+ }
+ for (const auto& artist : track->getArtists())
+ {
+ _artistPositions[artist.id()].insert(position);
+ _artistsMap[position].insert(artist.id());
+ }
+ }
+ }
+
+ _network = std::make_unique(std::move(network));
+
+ LMS_LOG(RECOMMENDATION, INFO) << "Classifier successfully initialized!";
+
+ return true;
+}
+
+std::unordered_set
+FeaturesClassifier::getMatchingRefVectorsPosition(const std::unordered_set& ids, const ObjectPositions& objectPositions)
+{
+ std::unordered_set res;
+
+ if (ids.empty())
+ return res;
+
+ for (auto id : ids)
+ {
+ auto it = objectPositions.find(id);
+ if (it == objectPositions.end())
+ continue;
+
+ for (const auto& position : it->second)
+ res.insert(position);
+ }
+
+ return res;
+}
+
+std::unordered_set
+FeaturesClassifier::getObjectsIds(const std::unordered_set& positionSet, const MatrixOfObjects& objectsMap)
+{
+ std::unordered_set res;
+
+ for (const auto& position : positionSet)
+ {
+ for (auto id : objectsMap.get(position))
+ res.insert(id);
+ }
+
+ return res;
+}
+
+std::vector
+FeaturesClassifier::getSimilarObjects(const std::unordered_set& ids,
+ const MatrixOfObjects& objectsMap,
+ const ObjectPositions& objectPosition,
+ std::size_t maxCount) const
+{
+ std::vector res;
+
+ std::unordered_set searchedRefVectorsPosition {getMatchingRefVectorsPosition(ids, objectPosition)};
+ if (searchedRefVectorsPosition.empty())
+ return res;
+
+ while (1)
+ {
+ std::unordered_set closestObjectIds {getObjectsIds(searchedRefVectorsPosition, objectsMap)};
+
+ // Remove objects that are already in input or already reported
+ for (auto id : ids)
+ closestObjectIds.erase(id);
+
+ {
+ std::vector objectIdsToAdd {std::cbegin(closestObjectIds), std::cend(closestObjectIds)};
+ Random::shuffleContainer(objectIdsToAdd );
+ std::copy(std::cbegin(objectIdsToAdd), std::cend(objectIdsToAdd), std::back_inserter(res));
+ }
+
+ if (res.size() > maxCount)
+ res.resize(maxCount);
+
+ if (res.size() == maxCount)
+ break;
+
+ // If there is not enough objects, try again with closest neighbour until there is too much distance
+ const std::optional closestRefVectorPosition {_network->getClosestRefVectorPosition(searchedRefVectorsPosition, _networkRefVectorsDistanceMedian * 0.75)};
+ if (!closestRefVectorPosition)
+ break;
+
+ searchedRefVectorsPosition.insert(closestRefVectorPosition.value());
+ }
+
+ return res;
+}
+
+
+
+} // ns Recommendation
diff --git a/src/libs/recommendation/impl/features/FeaturesClassifier.hpp b/src/libs/recommendation/impl/features/FeaturesClassifier.hpp
new file mode 100644
index 00000000..d4856a96
--- /dev/null
+++ b/src/libs/recommendation/impl/features/FeaturesClassifier.hpp
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2018 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+#include
+
+#include "recommendation/IClassifier.hpp"
+#include "som/DataNormalizer.hpp"
+#include "som/Network.hpp"
+#include "FeaturesClassifierCache.hpp"
+#include "FeaturesDefs.hpp"
+
+namespace Database
+{
+ class Session;
+}
+
+namespace Recommendation {
+
+using FeatureWeight = double;
+
+class FeaturesClassifier : public IClassifier
+{
+ public:
+ FeaturesClassifier() = default;
+ FeaturesClassifier(const FeaturesClassifier&) = delete;
+ FeaturesClassifier(FeaturesClassifier&&) = delete;
+ FeaturesClassifier& operator=(const FeaturesClassifier&) = delete;
+ FeaturesClassifier& operator=(FeaturesClassifier&&) = delete;
+
+ using FeaturesFetchFunc = std::function>>(Database::IdType /*trackId*/, const std::unordered_set& /*features*/)>;
+ // Default is to retrieve the features from the database (may be slow).
+ // Use this only if you want to train different searchers with some cached data
+ static void setFeaturesFetchFunc(FeaturesFetchFunc func) { _featuresFetchFunc = func; }
+
+ static const FeatureSettingsMap& getDefaultTrainFeatureSettings();
+ private:
+
+ std::string_view getName() const { return "Features"; }
+
+ bool init(Database::Session& session) override;
+ void requestCancelInit() override;
+
+ std::vector getSimilarTracksFromTrackList(Database::Session& session, Database::IdType tracklistId, std::size_t maxCount) const override;
+ std::vector getSimilarTracks(Database::Session& session, const std::unordered_set& tracksId, std::size_t maxCount) const override;
+ std::vector getSimilarReleases(Database::Session& session, Database::IdType releaseId, std::size_t maxCount) const;
+ std::vector getSimilarArtists(Database::Session& session, Database::IdType artistId, std::size_t maxCount) const;
+
+ bool initFromCache(Database::Session& session, const FeaturesClassifierCache& cache);
+
+ // Use training (may be very slow)
+ struct TrainSettings
+ {
+ std::size_t iterationCount {10};
+ float sampleCountPerNeuron {4};
+ FeatureSettingsMap featureSettingsMap;
+ };
+ bool initFromTraining(Database::Session& session, const TrainSettings& trainSettings);
+
+ using ObjectPositions = std::unordered_map>;
+ using MatrixOfObjects = SOM::Matrix>;
+
+ bool init(Database::Session& session,
+ SOM::Network network,
+ const ObjectPositions& tracksPosition);
+
+ FeaturesClassifierCache toCache() const;
+
+ static std::unordered_set getMatchingRefVectorsPosition(const std::unordered_set& ids, const ObjectPositions& objectPositions);
+ static std::unordered_set getObjectsIds(const std::unordered_set& positionSet, const MatrixOfObjects& objectsMap);
+
+ std::vector getSimilarObjects(const std::unordered_set& ids,
+ const SOM::Matrix>& objectsMap,
+ const ObjectPositions& objectPosition,
+ std::size_t maxCount) const;
+
+ bool _initCancelled {};
+ std::unique_ptr _network;
+ double _networkRefVectorsDistanceMedian {};
+
+ MatrixOfObjects _artistsMap;
+ ObjectPositions _artistPositions;
+
+ MatrixOfObjects _releasesMap;
+ ObjectPositions _releasePositions;
+
+ MatrixOfObjects _tracksMap;
+ ObjectPositions _trackPositions;
+
+ static inline FeaturesFetchFunc _featuresFetchFunc;
+};
+
+} // ns Recommendation
diff --git a/src/libs/recommendation/impl/features/SimilarityFeaturesCache.cpp b/src/libs/recommendation/impl/features/FeaturesClassifierCache.cpp
similarity index 76%
rename from src/libs/recommendation/impl/features/SimilarityFeaturesCache.cpp
rename to src/libs/recommendation/impl/features/FeaturesClassifierCache.cpp
index 235ed331..fc80098d 100644
--- a/src/libs/recommendation/impl/features/SimilarityFeaturesCache.cpp
+++ b/src/libs/recommendation/impl/features/FeaturesClassifierCache.cpp
@@ -17,7 +17,7 @@
* along with LMS. If not, see .
*/
-#include "SimilarityFeaturesCache.hpp"
+#include "FeaturesClassifierCache.hpp"
#include
#include
@@ -26,7 +26,7 @@
#include "utils/Logger.hpp"
#include "utils/Service.hpp"
-namespace Similarity {
+namespace Recommendation {
static
@@ -38,7 +38,7 @@ std::filesystem::path getCacheDirectory()
static std::filesystem::path getCacheNetworkFilePath()
{
return getCacheDirectory() / "network";
-};
+}
static std::filesystem::path getCacheTrackPositionsFilePath()
{
@@ -79,26 +79,25 @@ networkToCacheFile(const SOM::Network& network, std::filesystem::path path)
boost::property_tree::write_xml(path.string(), root);
- LMS_LOG(SIMILARITY, DEBUG) << "Created network cache";
+ LMS_LOG(RECOMMENDATION, DEBUG) << "Created network cache";
return true;
}
catch (boost::property_tree::ptree_error& error)
{
- LMS_LOG(SIMILARITY, ERROR) << "Cannot create network cache: " << error.what();
+ LMS_LOG(RECOMMENDATION, ERROR) << "Cannot create network cache: " << error.what();
return false;
}
}
-static
std::optional
-createNetworkFromCacheFile(const std::filesystem::path& path)
+FeaturesClassifierCache::createNetworkFromCacheFile(const std::filesystem::path& path)
{
if (!std::filesystem::exists(path))
return std::nullopt;
try
{
- LMS_LOG(SIMILARITY, INFO) << "Reading network from cache...";
+ LMS_LOG(RECOMMENDATION, INFO) << "Reading network from cache...";
boost::property_tree::ptree root;
@@ -132,20 +131,19 @@ createNetworkFromCacheFile(const std::filesystem::path& path)
res.setRefVector({x, y}, refVector);
}
- LMS_LOG(SIMILARITY, INFO) << "Successfully read network from cache";
+ LMS_LOG(RECOMMENDATION, INFO) << "Successfully read network from cache";
return res;
}
catch (boost::property_tree::ptree_error& error)
{
- LMS_LOG(SIMILARITY, ERROR) << "Cannot read network cache: " << error.what();
+ LMS_LOG(RECOMMENDATION, ERROR) << "Cannot read network cache: " << error.what();
return std::nullopt;
}
}
-static
bool
-objectPositionToCacheFile(const std::map>& objectsPosition, std::filesystem::path path)
+FeaturesClassifierCache::objectPositionToCacheFile(const ObjectPositions& objectsPosition, const std::filesystem::path& path)
{
try
{
@@ -174,24 +172,23 @@ objectPositionToCacheFile(const std::map>>
-createObjectPositionsFromCacheFile(std::filesystem::path path)
+std::optional
+FeaturesClassifierCache::createObjectPositionsFromCacheFile(const std::filesystem::path& path)
{
try
{
- LMS_LOG(SIMILARITY, INFO) << "Reading object position from cache...";
+ LMS_LOG(RECOMMENDATION, INFO) << "Reading object position from cache...";
boost::property_tree::ptree root;
boost::property_tree::read_xml(path.string(), root);
- std::map> res;
+ ObjectPositions res;
for (const auto& object : root.get_child("objects"))
{
@@ -205,42 +202,40 @@ createObjectPositionsFromCacheFile(std::filesystem::path path)
}
}
- LMS_LOG(SIMILARITY, INFO) << "Successfully read object position from cache";
+ LMS_LOG(RECOMMENDATION, INFO) << "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();
+ LMS_LOG(RECOMMENDATION, ERROR) << "Cannot create object position from cache file: " << error.what();
return std::nullopt;
}
}
void
-FeaturesCache::invalidate()
+FeaturesClassifierCache::invalidate()
{
std::filesystem::remove(getCacheNetworkFilePath());
std::filesystem::remove(getCacheTrackPositionsFilePath());
}
-std::optional
-FeaturesCache::read()
+std::optional
+FeaturesClassifierCache::read()
{
- std::optional res;
-
auto network{createNetworkFromCacheFile(getCacheNetworkFilePath())};
if (!network)
- return res;
+ return std::nullopt;
auto trackPositions{createObjectPositionsFromCacheFile(getCacheTrackPositionsFilePath())};
if (!trackPositions)
- return res;
+ return std::nullopt;
- return FeaturesCache{std::move(*network), std::move(*trackPositions)};
+ return FeaturesClassifierCache {std::move(*network), std::move(*trackPositions)};
}
void
-FeaturesCache::write()
+FeaturesClassifierCache::write() const
{
std::filesystem::create_directories(ServiceProvider::get()->getPath("working-dir") / "cache" / "features");
@@ -251,11 +246,10 @@ FeaturesCache::write()
}
}
-FeaturesCache::FeaturesCache(SOM::Network network, ObjectPositions trackPositions)
+FeaturesClassifierCache::FeaturesClassifierCache(SOM::Network network, ObjectPositions trackPositions)
: _network {std::move(network)},
_trackPositions {std::move(trackPositions)}
{
}
-
-} // namespace Similarity
+} // namespace Recommendation
diff --git a/src/libs/recommendation/impl/features/SimilarityFeaturesCache.hpp b/src/libs/recommendation/impl/features/FeaturesClassifierCache.hpp
similarity index 53%
rename from src/libs/recommendation/impl/features/SimilarityFeaturesCache.hpp
rename to src/libs/recommendation/impl/features/FeaturesClassifierCache.hpp
index a12334f3..25ddaeff 100644
--- a/src/libs/recommendation/impl/features/SimilarityFeaturesCache.hpp
+++ b/src/libs/recommendation/impl/features/FeaturesClassifierCache.hpp
@@ -19,33 +19,36 @@
#pragma once
-#include