Added distance to previous in the recommendation outputs
This commit is contained in:
+38
-31
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user