/* * 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 "AudioSimilarityEngine.hpp" #include #include #include #include #include #include #include #include "core/ILogger.hpp" #include "core/ITraceLogger.hpp" #include "core/Random.hpp" #include "database/IDb.hpp" #include "database/Session.hpp" #include "database/objects/Artist.hpp" #include "database/objects/Release.hpp" #include "database/objects/ReleaseArtistLink.hpp" #include "database/objects/Track.hpp" #include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackList.hpp" #include "database/objects/TrackMusicNNEmbeddings.hpp" #include "math/ChamferDistance.hpp" #include "math/CovarianceCalculator.hpp" #include "math/MedoidCalculator.hpp" #include "math/NormalizedCosineDistance.hpp" #include "math/PrincipalComponents.hpp" #include "math/StatsAccumulator.hpp" #include "track-selection-constraints/DuplicateTrackConstraint.hpp" #include "track-selection-constraints/InterpolationFitConstraint.hpp" #include "track-selection-constraints/MaxDistanceConstraint.hpp" #include "track-selection-constraints/SameArtistConstraint.hpp" #include "track-selection-constraints/SameReleaseConstraint.hpp" #include "track-selection-constraints/SmoothTransitionConstraint.hpp" #include "Types.hpp" #define LOG(sev, message) LMS_LOG(RECOMMENDATION, sev, "[audio-similarity] " << message) namespace lms::recommendation { namespace detail { template TrackResults findNearestNeighbors( const ReducedVector& queryVector, // expected to be normalized const std::unordered_map& trackVectors, std::size_t maxNeighbors, db::TrackId excludeTrackId) { const math::NormalizedCosineDistance distFunc{ queryVector }; TrackResults neighbors; neighbors.reserve(trackVectors.size()); for (const auto& [trackId, trackVector] : trackVectors) { if (trackId == excludeTrackId) continue; neighbors.push_back({ .id = trackId, .distance = distFunc(*trackVector) }); } maxNeighbors = std::min(maxNeighbors, neighbors.size()); if (maxNeighbors == 0) return {}; std::nth_element(neighbors.begin(), neighbors.begin() + static_cast(maxNeighbors), neighbors.end(), [](const auto& lhs, const auto& rhs) { return lhs.distance < rhs.distance; }); neighbors.resize(maxNeighbors); std::sort(neighbors.begin(), neighbors.end(), [](const auto& lhs, const auto& rhs) { return lhs.distance < rhs.distance; }); return neighbors; } } // namespace detail template AudioSimilarityEngine::AudioSimilarityEngine(db::IDb& db) : _db{ db } { } template AudioSimilarityEngine::~AudioSimilarityEngine() = default; template void AudioSimilarityEngine::initializeConstraints() { constexpr float interpolationFitWeight{ 0.8F }; constexpr float smoothTransitionWeight{ 0.2F }; constexpr float sameReleaseWeight{ 0.5F }; constexpr float sameArtistWeight{ 0.5F }; _similarityEvaluator = {}; _similarityEvaluator.addHardConstraint(std::make_unique()); _similarityEvaluator.addHardConstraint(std::make_unique(_trackDistanceThreshold)); _similarityEvaluator.addSoftConstraint(std::make_unique(), interpolationFitWeight); _similarityEvaluator.addSoftConstraint(std::make_unique(), smoothTransitionWeight); _similarityEvaluator.addSoftConstraint(std::make_unique(_trackMetadata), sameReleaseWeight); _similarityEvaluator.addSoftConstraint(std::make_unique(_trackMetadata), sameArtistWeight); _pathEvaluator = {}; _pathEvaluator.addHardConstraint(std::make_unique()); _pathEvaluator.addSoftConstraint(std::make_unique(), interpolationFitWeight); _pathEvaluator.addSoftConstraint(std::make_unique(), smoothTransitionWeight); _pathEvaluator.addSoftConstraint(std::make_unique(_trackMetadata), sameReleaseWeight); _pathEvaluator.addSoftConstraint(std::make_unique(_trackMetadata), sameArtistWeight); } template TrackResults AudioSimilarityEngine::findSimilarTracksFromTrackList(db::TrackListId tracklistId, std::size_t maxCount) const { LMS_SCOPED_TRACE_DETAILED("AudioSimilarityEngine", "Find similar tracks from tracklist"); if (maxCount == 0) return {}; std::vector trackIds; { db::Session& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; const db::TrackList::pointer trackList{ db::TrackList::find(session, tracklistId) }; if (!trackList) return {}; trackIds = trackList->getTrackIds(); } if (trackIds.empty()) return {}; return findSimilarTracks(trackIds, maxCount); } template TrackResults AudioSimilarityEngine::findSimilarTracks(std::span tracksId, std::size_t maxCount) const { LMS_SCOPED_TRACE_DETAILED("AudioSimilarityEngine", "Find similar tracks"); TrackResults res; if (maxCount == 0 || tracksId.empty()) return res; math::MedoidCalculator medoidCalculator; for (const db::TrackId trackId : tracksId) { const auto it{ _trackVectors.find(trackId) }; if (it == _trackVectors.cend()) continue; medoidCalculator.add(*it->second); } if (medoidCalculator.empty()) return res; const ReducedVector queryVector{ medoidCalculator.finalize() }; const math::NormalizedCosineDistance distFunc{ queryVector }; using Distance = float; std::vector> rankedTracks; rankedTracks.reserve(_trackVectors.size()); for (const auto& [trackId, vectors] : _trackVectors) { if (std::find(std::cbegin(tracksId), std::cend(tracksId), trackId) != std::cend(tracksId)) continue; rankedTracks.emplace_back(trackId, distFunc(*vectors)); } // Oversample to give the diversity selection enough candidates to work with static constexpr std::size_t oversamplingFactor{ 5 }; const std::size_t candidateCount{ std::min(maxCount * oversamplingFactor, rankedTracks.size()) }; std::partial_sort(std::begin(rankedTracks), std::next(std::begin(rankedTracks), static_cast(candidateCount)), std::end(rankedTracks), [](const auto& lhs, const auto& rhs) { return lhs.second < rhs.second; }); rankedTracks.resize(candidateCount); // Greedy selection: at each step pick the candidate with the lowest penalized score. // distanceToPrevious is the cosine distance to the last selected track, so that // SmoothTransitionConstraint penalises large acoustic jumps between consecutive results. // Pre-seed selectedTracks with the input tracks so that soft constraints (same release, // same artist) treat them as already taken, preventing the first results from being // from the same release/artist as the inputs. std::vector selectedTracks(std::cbegin(tracksId), std::cend(tracksId)); selectedTracks.reserve(selectedTracks.size() + maxCount); res.reserve(maxCount); const ReducedVector* previousVector{}; while (res.size() < maxCount && !rankedTracks.empty()) { std::optional bestIdx; float bestScore{ std::numeric_limits::max() }; for (std::size_t i{}; i < rankedTracks.size(); ++i) { const auto& [candidateId, distanceToQuery]{ rankedTracks[i] }; const ReducedVector* candidateVector{ _trackVectors.at(candidateId) }; const float distanceToPrevious{ previousVector ? math::NormalizedCosineDistance{ *previousVector }(*candidateVector) : 0.F }; const TrackCandidateContext context{ .candidateTrackId = candidateId, .selectedTracks = selectedTracks, .distanceToQuery = distanceToQuery, .distanceToPrevious = distanceToPrevious, }; if (_similarityEvaluator.rejects(context)) continue; const float score{ _similarityEvaluator.score(context) }; if (score < bestScore) { bestScore = score; bestIdx = i; } } if (!bestIdx) break; const auto& [selectedId, distanceToQuery]{ rankedTracks[*bestIdx] }; res.push_back({ .id = selectedId, .distance = distanceToQuery }); selectedTracks.push_back(selectedId); previousVector = _trackVectors.at(selectedId); rankedTracks.erase(std::begin(rankedTracks) + static_cast(*bestIdx)); } return res; } template TrackResults AudioSimilarityEngine::findTrackSimilarityPath(db::TrackId startTrackId, db::TrackId endTrackId, std::size_t maxCount) const { LMS_SCOPED_TRACE_DETAILED("AudioSimilarityEngine", "Find track similarity path"); if (maxCount == 0) return {}; const auto itStart{ _trackVectors.find(startTrackId) }; const auto itEnd{ _trackVectors.find(endTrackId) }; if (itStart == _trackVectors.cend() || itEnd == _trackVectors.cend()) return {}; const ReducedVector startVector{ *itStart->second }; const ReducedVector endVector{ *itEnd->second }; const ReducedVector direction{ endVector - startVector }; std::vector path; path.reserve(maxCount); path.push_back(startTrackId); const ReducedVector* previousVector{ itStart->second }; static constexpr std::size_t DefaultNeighborCount{ 16 }; static constexpr std::size_t BroadNeighborCount{ 64 }; std::size_t neighborCount{ DefaultNeighborCount }; const std::size_t interiorCount{ (maxCount > 2) ? (maxCount - 2) : 0 }; auto evaluateCandidates = [&](const TrackResults& neighborList) -> std::optional { std::optional best; float bestScore{ std::numeric_limits::max() }; for (const auto& [candidateId, candidateDistance] : neighborList) { const auto* candidateVector{ _trackVectors.at(candidateId) }; const float transitionDistance{ math::NormalizedCosineDistance{ *previousVector }(*candidateVector) }; const TrackCandidateContext context{ .candidateTrackId = candidateId, .selectedTracks = path, .distanceToQuery = candidateDistance, .distanceToPrevious = transitionDistance, }; if (_pathEvaluator.rejects(context)) continue; const float score{ _pathEvaluator.score(context) }; if (score < bestScore) { bestScore = score; best = candidateId; } } return best; }; for (std::size_t i{}; i < interiorCount; ++i) { const float t{ static_cast(i + 1) / static_cast(interiorCount + 1) }; auto queryPoint{ startVector + direction * t }; queryPoint.normalizeL2(); const auto neighbors{ detail::findNearestNeighbors(queryPoint, _trackVectors, neighborCount, endTrackId) }; std::optional bestCandidate{ evaluateCandidates(neighbors) }; if (!bestCandidate && neighborCount < BroadNeighborCount) { neighborCount = BroadNeighborCount; const auto broaderNeighbors{ detail::findNearestNeighbors(queryPoint, _trackVectors, neighborCount, endTrackId) }; bestCandidate = evaluateCandidates(broaderNeighbors); } if (!bestCandidate) continue; path.push_back(*bestCandidate); previousVector = _trackVectors.at(*bestCandidate); } if (maxCount > 1) path.push_back(endTrackId); TrackResults results; results.reserve(path.size()); const math::NormalizedCosineDistance startDistFunc{ startVector }; for (const db::TrackId trackId : path) { const auto* trackVector{ _trackVectors.at(trackId) }; results.push_back({ .id = trackId, .distance = startDistFunc(*trackVector) }); } return results; } template ReleaseResults AudioSimilarityEngine::findSimilarReleases( db::ReleaseId releaseId, std::size_t maxCount) const { LMS_SCOPED_TRACE_DETAILED("AudioSimilarityEngine", "Find similar releases"); ResultContainer res; if (maxCount == 0) return res; const auto itQueryRelease{ _releaseVectors.find(releaseId) }; if (itQueryRelease == _releaseVectors.cend() || itQueryRelease->second.empty()) return res; const auto& queryReleaseFeatures{ itQueryRelease->second }; using Distance = float; std::vector> rankedReleases; rankedReleases.reserve(_releaseVectors.size()); using CosineDistance = math::NormalizedCosineDistance; for (const auto& [candidateId, candidateReleaseVectors] : _releaseVectors) { if (candidateId == releaseId || candidateReleaseVectors.empty()) continue; const FloatType distance{ math::symmetricalChamferDistance( queryReleaseFeatures, candidateReleaseVectors) }; if (distance <= _releaseDistanceThreshold) rankedReleases.emplace_back(candidateId, distance); } const std::size_t resultCount{ std::min(maxCount, rankedReleases.size()) }; std::partial_sort(std::begin(rankedReleases), std::next(std::begin(rankedReleases), resultCount), std::end(rankedReleases), [](const auto& lhs, const auto& rhs) { return lhs.second < rhs.second; }); res.reserve(resultCount); for (std::size_t i{}; i < resultCount; ++i) res.push_back({ .id = rankedReleases[i].first, .distance = rankedReleases[i].second }); return res; } template ArtistResults AudioSimilarityEngine::findSimilarArtists(db::ArtistId artistId, core::EnumSet linkTypes, std::size_t maxCount) const { LMS_SCOPED_TRACE_DETAILED("AudioSimilarityEngine", "Find similar artists"); ArtistResults res; if (maxCount == 0) return res; if (!linkTypes.contains(db::TrackArtistLinkType::Artist)) return res; const auto itQueryArtist{ _artistVectors.find(artistId) }; if (itQueryArtist == _artistVectors.cend() || itQueryArtist->second.empty()) return res; const auto& queryArtistFeatures{ itQueryArtist->second }; using Distance = float; std::vector> rankedArtists; rankedArtists.reserve(_artistVectors.size()); using CosineDistance = math::NormalizedCosineDistance; for (const auto& [candidateId, candidateArtistFeatures] : _artistVectors) { if (candidateId == artistId || candidateArtistFeatures.empty()) continue; const FloatType distance{ math::symmetricalChamferDistance( queryArtistFeatures, candidateArtistFeatures) }; if (distance <= _artistDistanceThreshold) rankedArtists.emplace_back(candidateId, distance); } const std::size_t resultCount{ std::min(maxCount, rankedArtists.size()) }; std::partial_sort(std::begin(rankedArtists), std::next(std::begin(rankedArtists), resultCount), std::end(rankedArtists), [](const auto& lhs, const auto& rhs) { return lhs.second < rhs.second; }); res.reserve(resultCount); for (std::size_t i{}; i < resultCount; ++i) res.push_back({ .id = rankedArtists[i].first, .distance = rankedArtists[i].second }); return res; } template void AudioSimilarityEngine::load() { LMS_SCOPED_TRACE_OVERVIEW("AudioSimilarityEngine", "Loading"); LOG(INFO, "loading..."); computeDatasetStats(); computeReducedFeatures(); computeTrackDistanceThreshold(); computeReleaseDistanceThreshold(); computeArtistDistanceThreshold(); initializeConstraints(); LOG(INFO, "loading complete!"); } template void AudioSimilarityEngine::computeDatasetStats() { LMS_SCOPED_TRACE_DETAILED("AudioSimilarityEngine", "Compute dataset stats"); LOG(DEBUG, "computing dataset stats..."); _pcaReady = false; _trackCount = 0; std::array, SourceDimCount> statsAccumulators; { db::Session& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; Provider::visitVectors(session, [&]([[maybe_unused]] db::TrackId trackId, const SourceVector& sourceVector) { for (std::size_t i{}; i < SourceDimCount; ++i) statsAccumulators[i].add(sourceVector[i]); _trackCount++; }); } for (std::size_t featureIndex{}; featureIndex < SourceDimCount; ++featureIndex) _sourceMeans[featureIndex] = static_cast(statsAccumulators[featureIndex].getMean()); // Compute covariance using AudioFeatureMatrix = math::SquareMatrix; const auto covariance{ std::make_unique() }; { const auto calculator{ std::make_unique>() }; db::Session& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; Provider::visitVectors(session, [&](db::TrackId, SourceVector& sourceVector) { for (std::size_t i{}; i < SourceDimCount; ++i) sourceVector[i] -= _sourceMeans[i]; calculator->add(sourceVector); }); calculator->finalizeSample(*covariance); } // PCA via power iteration + deflation in double precision { using EigenMatrix = math::SquareMatrix; using EigenVector = math::Vector; auto covarianceCopy{ std::make_unique() }; for (std::size_t i{}; i < SourceDimCount; ++i) { for (std::size_t j{}; j < SourceDimCount; ++j) (*covarianceCopy)[i][j] = static_cast((*covariance)[i][j]); } EigenVector eigenValues{}; auto eigenVectors{ std::make_unique>() }; math::computeEigenpairsViaPowerIteration(*covarianceCopy, *eigenVectors, eigenValues); // Store PCA basis and whitening scales for (std::size_t k{}; k < ReducedDimCount; ++k) { for (std::size_t j{}; j < SourceDimCount; ++j) _pcaBasis[k][j] = static_cast((*eigenVectors)[k][j]); _pcaScale[k] = (eigenValues[k] > 1e-15) ? static_cast(1.0 / std::sqrt(eigenValues[k])) : FloatType{}; } } _pcaReady = true; LOG(DEBUG, "computing dataset stats done"); } template void AudioSimilarityEngine::getReducedVector(const SourceVector& sourceVector, ReducedVector& reducedVector) const { SourceVector centeredSourceVector{ sourceVector }; for (std::size_t i{}; i < SourceDimCount; ++i) centeredSourceVector[i] -= _sourceMeans[i]; projectToReduced(centeredSourceVector, reducedVector); reducedVector.normalizeL2(); } template void AudioSimilarityEngine::projectToReduced(const SourceVector& sourceVectorCentered, ReducedVector& reducedVector) const { assert(_pcaReady); math::projectOntoBasis(_pcaBasis, sourceVectorCentered, reducedVector, _pcaScale); } template void AudioSimilarityEngine::computeReducedFeatures() { LMS_SCOPED_TRACE_DETAILED("AudioSimilarityEngine", "ComputeReducedVectors"); LOG(INFO, "computing reduced vectors... Reducing from " << SourceDimCount << " to " << ReducedDimCount << " dimensions"); db::Session& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; _trackVectors.clear(); _vectors.clear(); _vectors.reserve(_trackCount); // must keep pointers valid _releaseVectors.clear(); _artistVectors.clear(); _trackMetadata.clear(); Provider::visitVectors(session, [&](db::TrackId trackId, const SourceVector& sourceVector) { if (_vectors.size() >= _trackCount) return; // more tracks appeared since computeDatasetStats(); skip to avoid reallocation (a further reload will include them) auto& reducedVector{ _vectors.emplace_back() }; getReducedVector(sourceVector, reducedVector); _trackVectors.try_emplace(trackId, &reducedVector); }); db::Release::find(session, db::Release::FindParameters{}, [&](const db::Release::pointer& release) { std::vector> releaseTrackFeatures; db::Track::FindParameters params; params.setRelease(release->getId()); const auto trackIds{ db::Track::findIds(session, params) }; for (const db::TrackId trackId : trackIds.results) { const auto itFeatures{ _trackVectors.find(trackId) }; if (itFeatures != std::cend(_trackVectors)) { assert(itFeatures->second); releaseTrackFeatures.emplace_back(*itFeatures->second); _trackMetadata[trackId].releaseId = release->getId(); } } if (!releaseTrackFeatures.empty()) _releaseVectors.try_emplace(release->getId(), std::move(releaseTrackFeatures)); }); db::Artist::find(session, db::Artist::FindParameters{}, [&](const db::Artist::pointer& artist) { std::unordered_set artistTrackIds; // Track-level artists { db::Track::FindParameters params; params.setArtist(artist->getId(), { db::TrackArtistLinkType::Artist }); for (const db::TrackId trackId : db::Track::findIds(session, params).results) artistTrackIds.insert(trackId); } // Album-level artists { db::Release::FindParameters params; params.setArtist(artist->getId()); for (const db::ReleaseId releaseId : db::Release::findIds(session, params).results) { if (_releaseVectors.contains(releaseId)) { db::Track::FindParameters trackParams; trackParams.setRelease(releaseId); for (const db::TrackId trackId : db::Track::findIds(session, trackParams).results) artistTrackIds.insert(trackId); } } } // Build vectors from deduplicated track IDs std::vector> artistTrackVectors; artistTrackVectors.reserve(artistTrackIds.size()); for (const db::TrackId trackId : artistTrackIds) { const auto it{ _trackVectors.find(trackId) }; if (it != std::cend(_trackVectors)) { assert(it->second); artistTrackVectors.emplace_back(*it->second); _trackMetadata[trackId].artistIds.push_back(artist->getId()); } } if (!artistTrackVectors.empty()) _artistVectors.try_emplace(artist->getId(), std::move(artistTrackVectors)); }); // Sort artistIds in each TrackMetadata entry for set-intersection in SameArtistConstraint for (auto& [trackId, metadata] : _trackMetadata) std::sort(metadata.artistIds.begin(), metadata.artistIds.end()); LOG(INFO, "computed reduced vectors: " << _trackVectors.size() << " tracks, " << _releaseVectors.size() << " releases, " << _artistVectors.size() << " artists"); } template void AudioSimilarityEngine::computeTrackDistanceThreshold() { LMS_SCOPED_TRACE_DETAILED("AudioSimilarityEngine", "computeTrackDistanceThreshold"); constexpr std::size_t maxSampleCount{ 500 }; constexpr float stdDevMultiplier{ 2.F }; const std::size_t sampleCount{ std::min(_trackVectors.size(), maxSampleCount) }; LOG(INFO, "computing track distance threshold using " << sampleCount << " samples..."); // Collect all vector pointers and shuffle for an unbiased random sample. std::vector allVectors; allVectors.reserve(_trackVectors.size()); for (const auto& [id, vec] : _trackVectors) allVectors.push_back(vec); std::minstd_rand randomEngine{ 42 }; core::random::shuffleContainer(randomEngine, allVectors); math::StatsAccumulator stats; for (std::size_t i{}; i < sampleCount; ++i) { const ReducedVector* queryVector{ allVectors[i] }; const math::NormalizedCosineDistance distFunc{ *queryVector }; FloatType minDist{ std::numeric_limits::max() }; for (const ReducedVector* candidateVector : allVectors) { if (candidateVector == queryVector) continue; const FloatType d{ distFunc(*candidateVector) }; if (d < minDist) minDist = d; } stats.add(minDist); } if (stats.getCount() >= 2) _trackDistanceThreshold = stats.getMean() + stdDevMultiplier * stats.getSampleStdDev(); else _trackDistanceThreshold = std::numeric_limits::max(); LOG(INFO, "track distance threshold = " << _trackDistanceThreshold); } template void AudioSimilarityEngine::computeReleaseDistanceThreshold() { LMS_SCOPED_TRACE_DETAILED("AudioSimilarityEngine", "ComputeReleaseDistanceThreshold"); constexpr std::size_t maxSampleCount{ 200 }; constexpr float stdDevMultiplier{ 2.F }; using CosineDistance = math::NormalizedCosineDistance; std::vector>*> allProfiles; allProfiles.reserve(_releaseVectors.size()); for (const auto& [id, vecs] : _releaseVectors) allProfiles.push_back(&vecs); const std::size_t sampleCount{ std::min(allProfiles.size(), maxSampleCount) }; LOG(INFO, "computing release distance threshold using " << sampleCount << " samples..."); std::minstd_rand randomEngine{ 42 }; core::random::shuffleContainer(randomEngine, allProfiles); math::StatsAccumulator stats; for (std::size_t i{}; i < sampleCount; ++i) { FloatType minDist{ std::numeric_limits::max() }; for (const auto* candidate : allProfiles) { if (candidate == allProfiles[i]) continue; const FloatType d{ math::symmetricalChamferDistance(*allProfiles[i], *candidate) }; if (d < minDist) minDist = d; } if (minDist < std::numeric_limits::max()) stats.add(minDist); } if (stats.getCount() >= 2) _releaseDistanceThreshold = stats.getMean() + stdDevMultiplier * stats.getSampleStdDev(); else _releaseDistanceThreshold = std::numeric_limits::max(); LOG(INFO, "release distance threshold = " << _releaseDistanceThreshold); } template void AudioSimilarityEngine::computeArtistDistanceThreshold() { LMS_SCOPED_TRACE_DETAILED("AudioSimilarityEngine", "ComputeArtistDistanceThreshold"); constexpr std::size_t maxSampleCount{ 200 }; constexpr float stdDevMultiplier{ 2.F }; using CosineDistance = math::NormalizedCosineDistance; std::vector>*> allProfiles; allProfiles.reserve(_artistVectors.size()); for (const auto& [id, vecs] : _artistVectors) allProfiles.push_back(&vecs); const std::size_t sampleCount{ std::min(allProfiles.size(), maxSampleCount) }; LOG(INFO, "computing artist distance threshold using " << sampleCount << " samples..."); std::minstd_rand randomEngine{ 42 }; core::random::shuffleContainer(randomEngine, allProfiles); math::StatsAccumulator stats; for (std::size_t i{}; i < sampleCount; ++i) { FloatType minDist{ std::numeric_limits::max() }; for (const auto* candidate : allProfiles) { if (candidate == allProfiles[i]) continue; const FloatType d{ math::symmetricalChamferDistance(*allProfiles[i], *candidate) }; if (d < minDist) minDist = d; } if (minDist < std::numeric_limits::max()) stats.add(minDist); } if (stats.getCount() >= 2) _artistDistanceThreshold = stats.getMean() + stdDevMultiplier * stats.getSampleStdDev(); else _artistDistanceThreshold = std::numeric_limits::max(); LOG(INFO, "artist distance threshold = " << _artistDistanceThreshold); } } // namespace lms::recommendation #undef LOG