Added distance to previous in the recommendation outputs

This commit is contained in:
emeric
2026-06-10 18:38:05 +02:00
parent 1d75add3d8
commit a826a70c68
5 changed files with 54 additions and 46 deletions
@@ -64,21 +64,27 @@ namespace lms::recommendation
{
namespace detail
{
struct TrackNeighbor
{
db::TrackId id;
float distance{};
};
template<typename ReducedVector>
TrackResults findNearestNeighbors(
std::vector<TrackNeighbor> findNearestNeighbors(
const ReducedVector& queryVector, // expected to be normalized
const std::unordered_map<db::TrackId, const ReducedVector*>& trackVectors,
std::size_t maxNeighbors,
db::TrackId excludeTrackId)
std::span<const db::TrackId> excludeTrackIds)
{
const math::NormalizedCosineDistance distFunc{ queryVector };
TrackResults neighbors;
std::vector<TrackNeighbor> neighbors;
neighbors.reserve(trackVectors.size());
for (const auto& [trackId, trackVector] : trackVectors)
{
if (trackId == excludeTrackId)
if (std::find(std::cbegin(excludeTrackIds), std::cend(excludeTrackIds), trackId) != std::cend(excludeTrackIds))
continue;
neighbors.push_back({ .id = trackId, .distance = distFunc(*trackVector) });
@@ -186,27 +192,10 @@ namespace lms::recommendation
return res;
const ReducedVector& queryVector{ *medoidCalculator.finalize() };
const math::NormalizedCosineDistance distFunc{ queryVector };
using Distance = float;
std::vector<std::pair<db::TrackId, Distance>> 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<std::ptrdiff_t>(candidateCount)), std::end(rankedTracks), [](const auto& lhs, const auto& rhs) {
return lhs.second < rhs.second;
});
rankedTracks.resize(candidateCount);
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,
@@ -216,6 +205,17 @@ namespace lms::recommendation
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<std::size_t> bestIdx;
@@ -223,7 +223,7 @@ namespace lms::recommendation
for (std::size_t i{}; i < rankedTracks.size(); ++i)
{
const db::TrackId candidateId{ rankedTracks[i].first };
const db::TrackId candidateId{ rankedTracks[i].id };
const TrackCandidateContext context{
.candidateTrackId = candidateId,
@@ -246,8 +246,11 @@ namespace lms::recommendation
break;
const auto& [selectedId, distanceToQuery]{ rankedTracks[*bestIdx] };
res.push_back({ .id = selectedId, .distance = distanceToQuery });
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<std::ptrdiff_t>(*bestIdx));
}
@@ -284,17 +287,17 @@ namespace lms::recommendation
auto queryPoint{ startVector + direction * t };
queryPoint.normalizeL2();
const auto neighbors{ detail::findNearestNeighbors(queryPoint, _trackVectors, NeighborCount, endTrackId) };
const auto neighbors{ detail::findNearestNeighbors(queryPoint, _trackVectors, NeighborCount, std::span<const db::TrackId>{ &endTrackId, 1 }) };
const db::TrackId stepSeedTrackId{ neighbors.empty() ? startTrackId : neighbors[0].id };
const std::array<db::TrackId, 1> stepSeedTrackIds{ stepSeedTrackId };
std::optional<db::TrackId> best;
float bestScore{ std::numeric_limits<float>::max() };
for (const auto& [candidateTrackId, candidateDistance] : neighbors)
for (const auto& neighbor : neighbors)
{
const TrackCandidateContext context{
.candidateTrackId = candidateTrackId,
.candidateTrackId = neighbor.id,
.selectedTracks = path,
.seedTrackIds = stepSeedTrackIds,
};
@@ -306,7 +309,7 @@ namespace lms::recommendation
if (score < bestScore)
{
bestScore = score;
best = candidateTrackId;
best = neighbor.id;
}
}
@@ -323,10 +326,14 @@ namespace lms::recommendation
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) };
results.push_back({ .id = trackId, .distance = startDistFunc(*trackVector) });
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;
@@ -384,7 +391,7 @@ namespace lms::recommendation
res.reserve(resultCount);
for (std::size_t i{}; i < resultCount; ++i)
res.push_back({ .id = rankedReleases[i].first, .distance = rankedReleases[i].second });
res.push_back({ .id = rankedReleases[i].first, .distanceToFirst = rankedReleases[i].second });
return res;
}
@@ -444,7 +451,7 @@ namespace lms::recommendation
res.reserve(resultCount);
for (std::size_t i{}; i < resultCount; ++i)
res.push_back({ .id = rankedArtists[i].first, .distance = rankedArtists[i].second });
res.push_back({ .id = rankedArtists[i].first, .distanceToFirst = rankedArtists[i].second });
return res;
}
@@ -86,7 +86,7 @@ namespace lms::recommendation
ResultContainer<IdType> res;
res.reserve(resultCount);
for (std::size_t i{}; i < resultCount; ++i)
res.push_back({ .id = overlapCounts[i].first, .distance = {} });
res.push_back({ .id = overlapCounts[i].first, .distanceToFirst = {}, .distanceToPrevious = {} });
return res;
}
@@ -295,7 +295,7 @@ namespace lms::recommendation
if (!bestIdx)
break;
res.push_back({ .id = candidates[*bestIdx], .distance = {} });
res.push_back({ .id = candidates[*bestIdx], .distanceToFirst = {}, .distanceToPrevious = {} });
selectedTracks.push_back(candidates[*bestIdx]);
candidates.erase(std::begin(candidates) + static_cast<std::ptrdiff_t>(*bestIdx));
}
@@ -364,7 +364,7 @@ namespace lms::recommendation
return {};
if (startTrackId == endTrackId)
return { RecommendationResult<db::TrackId>{ .id = startTrackId, .distance = {} } };
return { RecommendationResult<db::TrackId>{ .id = startTrackId, .distanceToFirst = {}, .distanceToPrevious = {} } };
db::Session& dbSession{ _db.getTLSSession() };
auto transaction{ dbSession.createReadTransaction() };
@@ -376,9 +376,9 @@ namespace lms::recommendation
TrackResults res;
res.reserve(std::min<std::size_t>(maxCount, 2));
res.push_back({ .id = startTrackId, .distance = {} });
res.push_back({ .id = startTrackId, .distanceToFirst = {}, .distanceToPrevious = {} });
if (maxCount > 1)
res.push_back({ .id = endTrackId, .distance = {} });
res.push_back({ .id = endTrackId, .distanceToFirst = {}, .distanceToPrevious = {} });
return res;
}
@@ -31,7 +31,8 @@ namespace lms::recommendation
struct RecommendationResult
{
IdType id;
float distance{}; // normalized distance in [0, 1]: 0 = most similar, 1 = least similar
float distanceToFirst{}; // normalized [0, 1]: 0 = most similar, 1 = least similar (distance to seed/query)
float distanceToPrevious{}; // normalized [0, 1]: 0 = most similar, 1 = least similar (distance to preceding result)
};
template<typename IdType>
@@ -696,7 +696,7 @@ namespace lms::api::subsonic
if (track)
{
Response::Node& sonicMatchNode{ response.createArrayNode("sonicMatch") };
sonicMatchNode.setAttribute("similarity", 1.0F - similarTrack.distance);
sonicMatchNode.setAttribute("similarity", 1.0F - similarTrack.distanceToFirst);
sonicMatchNode.addChild("entry", createSongNode(context, track, context.getUser()));
}
}
@@ -727,7 +727,7 @@ namespace lms::api::subsonic
if (track)
{
Response::Node& sonicMatchNode{ response.createArrayNode("sonicMatch") };
sonicMatchNode.setAttribute("similarity", 1.0F - pathTrack.distance);
sonicMatchNode.setAttribute("similarity", 1.0F - pathTrack.distanceToFirst);
sonicMatchNode.addChild("entry", createSongNode(context, track, context.getUser()));
}
}
@@ -86,7 +86,7 @@ namespace lms
std::cout << "Processing track " << trackToString(trackId) << std::endl;
for (const auto& similarTrack : recommendationService.findSimilarTracks(std::span<const db::TrackId>{ &trackId, 1 }, maxCount))
std::cout << "\t- " << similarTrack.distance << ", Similar track " << trackToString(similarTrack.id) << std::endl;
std::cout << "\t- " << "Similar track " << trackToString(similarTrack.id) << " (first: " << similarTrack.distanceToFirst << ", prev: " << similarTrack.distanceToPrevious << ")" << std::endl;
}
}
@@ -128,7 +128,7 @@ namespace lms
std::cout << "Processing release '" << releaseToString(releaseId) << "'" << std::endl;
for (const auto& similarRelease : recommendationService.findSimilarReleases(releaseId, maxCount))
std::cout << "\t- " << similarRelease.distance << ", Similar release " << releaseToString(similarRelease.id) << std::endl;
std::cout << "\t- " << "Similar release " << releaseToString(similarRelease.id) << " (first: " << similarRelease.distanceToFirst << ", prev: " << similarRelease.distanceToPrevious << ")" << std::endl;
}
}
@@ -163,7 +163,7 @@ namespace lms
std::cout << "Processing artist '" << artistToString(artistId) << "'" << std::endl;
for (const auto& similarArtist : recommendationService.findSimilarArtists(artistId, { db::TrackArtistLinkType::Artist }, maxCount))
std::cout << "\t- " << similarArtist.distance << ", Similar artist '" << artistToString(similarArtist.id) << "'" << std::endl;
std::cout << "\t- " << "Similar artist '" << artistToString(similarArtist.id) << "' (first: " << similarArtist.distanceToFirst << ", prev: " << similarArtist.distanceToPrevious << ")" << std::endl;
}
}
@@ -229,7 +229,7 @@ namespace lms
}
for (const auto& similarTrack : similarTracks)
std::cout << "\t- " << similarTrack.distance << ", Similar track " << trackToString(similarTrack.id) << std::endl;
std::cout << "\t- " << "Similar track " << trackToString(similarTrack.id) << " (first: " << similarTrack.distanceToFirst << ", prev: " << similarTrack.distanceToPrevious << ")" << std::endl;
}
}
@@ -290,7 +290,7 @@ namespace lms
}
for (const auto& similarRelease : similarReleases)
std::cout << "\t- " << similarRelease.distance << ", Similar release " << releaseToString(similarRelease.id) << std::endl;
std::cout << "\t- " << "Similar release " << releaseToString(similarRelease.id) << " (first: " << similarRelease.distanceToFirst << ", prev: " << similarRelease.distanceToPrevious << ")" << std::endl;
}
}
@@ -345,7 +345,7 @@ namespace lms
}
for (const auto& similarArtist : similarArtists)
std::cout << "\t- " << similarArtist.distance << ", Similar artist '" << artistToString(similarArtist.id) << "'" << std::endl;
std::cout << "\t- " << "Similar artist '" << artistToString(similarArtist.id) << "' (first: " << similarArtist.distanceToFirst << ", prev: " << similarArtist.distanceToPrevious << ")" << std::endl;
}
}
@@ -436,7 +436,7 @@ namespace lms
for (std::size_t i{}; i < path.size(); ++i)
{
const auto& result{ path[i] };
std::cout << "\t" << (i + 1) << ". " << trackToString(result.id) << " (distance: " << result.distance << ")" << std::endl;
std::cout << "\t" << (i + 1) << ". " << trackToString(result.id) << " (first: " << result.distanceToFirst << ", prev: " << result.distanceToPrevious << ")" << std::endl;
}
}
}