/* * 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 "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 "InterpolationFitConstraint.hpp" #include "MaxDistanceConstraint.hpp" #include "SmoothTransitionConstraint.hpp" #include "track-selection-constraints/DuplicateTrackConstraint.hpp" #include "track-selection-constraints/SameArtistConstraint.hpp" #include "track-selection-constraints/SameRecordingMBIDConstraint.hpp" #include "track-selection-constraints/SameReleaseConstraint.hpp" #include "Types.hpp" #define LOG(sev, message) LMS_LOG(RECOMMENDATION, sev, "[audio-similarity] " << message) namespace lms::recommendation { namespace detail { struct TrackNeighbor { db::TrackId id; float distance{}; }; template std::vector findNearestNeighbors( const ReducedVector& queryVector, // expected to be normalized const std::unordered_map& trackVectors, std::size_t maxNeighbors, std::span excludeTrackIds) { const math::NormalizedCosineDistance distFunc{ queryVector }; std::vector neighbors; neighbors.reserve(trackVectors.size()); for (const auto& [trackId, trackVector] : trackVectors) { if (std::find(std::cbegin(excludeTrackIds), std::cend(excludeTrackIds), trackId) != std::cend(excludeTrackIds)) 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(_trackMetadata)); _similarityEvaluator.addHardConstraint(std::make_unique>(_trackVectors, _trackDistanceThreshold)); _similarityEvaluator.addSoftConstraint(std::make_unique>(_trackVectors), interpolationFitWeight); _similarityEvaluator.addSoftConstraint(std::make_unique>(_trackVectors), smoothTransitionWeight); _similarityEvaluator.addSoftConstraint(std::make_unique(_trackMetadata), sameReleaseWeight); _similarityEvaluator.addSoftConstraint(std::make_unique(_trackMetadata), sameArtistWeight); _pathEvaluator = {}; _pathEvaluator.addHardConstraint(std::make_unique()); _pathEvaluator.addHardConstraint(std::make_unique(_trackMetadata)); _pathEvaluator.addSoftConstraint(std::make_unique>(_trackVectors), interpolationFitWeight); _pathEvaluator.addSoftConstraint(std::make_unique>(_trackVectors), 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() }; // Oversample to give the diversity selection enough candidates to work with static constexpr std::size_t oversamplingFactor{ 5 }; auto rankedTracks{ detail::findNearestNeighbors(queryVector, _trackVectors, maxCount * oversamplingFactor, tracksId) }; // Greedy selection: at each step pick the candidate with the lowest penalized score. // 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* prevVector{ nullptr }; for (auto it{ tracksId.rbegin() }; it != tracksId.rend(); ++it) { const auto found{ _trackVectors.find(*it) }; if (found != _trackVectors.cend()) { prevVector = found->second; break; } } 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 db::TrackId candidateId{ rankedTracks[i].id }; const TrackCandidateContext context{ .candidateTrackId = candidateId, .selectedTracks = selectedTracks, .seedTrackIds = tracksId, }; 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] }; const auto* selectedVector{ _trackVectors.at(selectedId) }; const float distanceToPrev{ prevVector ? math::NormalizedCosineDistance{ *prevVector }(*selectedVector) : distanceToQuery }; res.push_back({ .id = selectedId, .distanceToFirst = distanceToQuery, .distanceToPrevious = distanceToPrev }); selectedTracks.push_back(selectedId); prevVector = selectedVector; 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); static constexpr std::size_t NeighborCount{ 32 }; const std::size_t interiorCount{ (maxCount > 2) ? (maxCount - 2) : 0 }; 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, std::span{ &endTrackId, 1 }) }; const db::TrackId stepSeedTrackId{ neighbors.empty() ? startTrackId : neighbors[0].id }; const std::array stepSeedTrackIds{ stepSeedTrackId }; std::optional best; float bestScore{ std::numeric_limits::max() }; for (const auto& neighbor : neighbors) { const TrackCandidateContext context{ .candidateTrackId = neighbor.id, .selectedTracks = path, .seedTrackIds = stepSeedTrackIds, }; if (_pathEvaluator.rejects(context)) continue; const float score{ _pathEvaluator.score(context) }; if (score < bestScore) { bestScore = score; best = neighbor.id; } } if (!best) continue; path.push_back(*best); } if (maxCount > 1) path.push_back(endTrackId); TrackResults results; results.reserve(path.size()); const math::NormalizedCosineDistance startDistFunc{ startVector }; const ReducedVector* prevVector{ &startVector }; for (const db::TrackId trackId : path) { const auto* trackVector{ _trackVectors.at(trackId) }; const float distToFirst{ startDistFunc(*trackVector) }; const float distToPrev{ math::NormalizedCosineDistance{ *prevVector }(*trackVector) }; results.push_back({ .id = trackId, .distanceToFirst = distToFirst, .distanceToPrevious = distToPrev }); prevVector = 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; using CosineDistance = math::NormalizedCosineDistance; // Stage 1: fast medoid scan to get top candidates constexpr std::size_t preFilterMultiplier{ 10 }; constexpr std::size_t preFilterMinCount{ 50 }; const std::size_t preFilterCount{ std::min(_releaseMedoids.size() - 1, std::max(preFilterMinCount, maxCount * preFilterMultiplier)) }; const math::NormalizedCosineDistance queryMedoidDist{ *_releaseMedoids.at(releaseId) }; std::vector> medoidCandidates; medoidCandidates.reserve(_releaseMedoids.size()); for (const auto& [candidateId, medoid] : _releaseMedoids) { if (candidateId != releaseId) medoidCandidates.emplace_back(candidateId, queryMedoidDist(*medoid)); } std::nth_element(medoidCandidates.begin(), std::next(medoidCandidates.begin(), preFilterCount), medoidCandidates.end(), [](const auto& a, const auto& b) { return a.second < b.second; }); medoidCandidates.resize(preFilterCount); // Stage 2: Chamfer re-rank on top candidates std::vector> rankedReleases; rankedReleases.reserve(preFilterCount); for (const auto& [candidateId, _] : medoidCandidates) { const FloatType distance{ math::symmetricalChamferDistance(queryReleaseFeatures, _releaseVectors.at(candidateId)) }; 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), static_cast(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, .distanceToFirst = 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; using CosineDistance = math::NormalizedCosineDistance; // Stage 1: fast medoid scan to get top-K candidates constexpr std::size_t preFilterMultiplier{ 10 }; constexpr std::size_t preFilterMinCount{ 50 }; const std::size_t preFilterCount{ std::min(_artistMedoids.size() - 1, std::max(preFilterMinCount, maxCount * preFilterMultiplier)) }; const math::NormalizedCosineDistance queryMedoidDist{ *_artistMedoids.at(artistId) }; std::vector> medoidCandidates; medoidCandidates.reserve(_artistMedoids.size()); for (const auto& [candidateId, medoid] : _artistMedoids) { if (candidateId != artistId) medoidCandidates.emplace_back(candidateId, queryMedoidDist(*medoid)); } std::nth_element(medoidCandidates.begin(), std::next(medoidCandidates.begin(), preFilterCount), medoidCandidates.end(), [](const auto& a, const auto& b) { return a.second < b.second; }); medoidCandidates.resize(preFilterCount); // Stage 2: Chamfer re-rank on top-K candidates std::vector> rankedArtists; rankedArtists.reserve(preFilterCount); for (const auto& [candidateId, _] : medoidCandidates) { const FloatType distance{ math::symmetricalChamferDistance(queryArtistFeatures, _artistVectors.at(candidateId)) }; 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, .distanceToFirst = 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(); _releaseMedoids.clear(); _artistVectors.clear(); _artistMedoids.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::Track::find(session, db::Track::FindParameters{}, [&](const db::Track::pointer& track) { const auto itVec{ _trackVectors.find(track->getId()) }; if (itVec == _trackVectors.cend()) return; auto& meta{ _trackMetadata[track->getId()] }; const db::ReleaseId releaseId{ track->getReleaseId() }; meta.releaseId = releaseId; meta.recordingMBID = track->getRecordingMBID(); if (releaseId.isValid()) _releaseVectors[releaseId].emplace_back(*itVec->second); }); math::MedoidCalculator calc; for (const auto& [id, vecs] : _releaseVectors) { calc.clear(); for (const auto& v : vecs) calc.add(v.get()); _releaseMedoids.try_emplace(id, calc.finalize()); } db::Artist::find(session, db::Artist::FindParameters{}, [&](const db::Artist::pointer& artist) { const auto mbid{ artist->getMBID() }; // skip "Various Artists" to avoid false artist matches if (mbid && mbid->toString() == "89ad4ac3-39f7-470e-963a-56509c546377") return; std::unordered_set artistTrackIds; { db::Release::FindParameters params; params.setArtist(artist->getId()); for (const db::ReleaseId releaseId : db::Release::findIds(session, params)) { if (_releaseVectors.contains(releaseId)) { db::Track::FindParameters trackParams; trackParams.setRelease(releaseId); for (const db::TrackId trackId : db::Track::findIds(session, trackParams)) 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)); }); for (const auto& [id, vecs] : _artistVectors) { calc.clear(); for (const auto& v : vecs) calc.add(v.get()); _artistMedoids.try_emplace(id, calc.finalize()); } // 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 std::size_t maxCandidateCount{ 10'000 }; constexpr float stdDevMultiplier{ 2.F }; std::vector allVectors; allVectors.reserve(_trackVectors.size()); for (const auto& [id, vec] : _trackVectors) allVectors.push_back(vec); // move maxCandidateCount random elements to the front const std::size_t candidateCount{ std::min(allVectors.size(), maxCandidateCount) }; std::minstd_rand randomEngine{ 42 }; for (std::size_t i{}; i < candidateCount; ++i) { std::uniform_int_distribution dist{ i, allVectors.size() - 1 }; std::swap(allVectors[i], allVectors[dist(randomEngine)]); } allVectors.resize(candidateCount); const std::size_t sampleCount{ std::min(candidateCount, maxSampleCount) }; LOG(INFO, "computing track distance threshold using " << sampleCount << " samples on " << candidateCount << " candidates..."); 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; } if (minDist < std::numeric_limits::max()) 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 std::size_t maxCandidateCount{ 2'000 }; 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); // move maxCandidateCount random elements to the front const std::size_t candidateCount{ std::min(allProfiles.size(), maxCandidateCount) }; std::minstd_rand randomEngine{ 42 }; for (std::size_t i{}; i < candidateCount; ++i) { std::uniform_int_distribution dist{ i, allProfiles.size() - 1 }; std::swap(allProfiles[i], allProfiles[dist(randomEngine)]); } allProfiles.resize(candidateCount); const std::size_t sampleCount{ std::min(candidateCount, maxSampleCount) }; LOG(INFO, "computing release distance threshold using " << sampleCount << " samples on " << candidateCount << " candidates..."); 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 std::size_t maxCandidateCount{ 2'000 }; 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); // move maxCandidateCount random elements to the front const std::size_t candidateCount{ std::min(allProfiles.size(), maxCandidateCount) }; std::minstd_rand randomEngine{ 42 }; for (std::size_t i{}; i < candidateCount; ++i) { std::uniform_int_distribution dist{ i, allProfiles.size() - 1 }; std::swap(allProfiles[i], allProfiles[dist(randomEngine)]); } allProfiles.resize(candidateCount); const std::size_t sampleCount{ std::min(candidateCount, maxSampleCount) }; LOG(INFO, "computing artist distance threshold using " << sampleCount << " samples on " << candidateCount << " candidates..."); 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