feat: gallery build report
GR-003 — the calibration fit already computed per-actor dedup counts, how many actors are eligible for positive pairs, and a 200-bin histogram of the intra and inter distributions, then discarded all of it to stderr. Nothing persisted, so nobody could audit whether a gallery was any good. The report is written alongside the gallery at build time. That is the right moment: the matcher fits the same sigmoid at analysis time, but by then the answer is per-run and nobody is looking, whereas build time is when a gallery's quality is actually decided. What it surfaces, in order of usefulness: - actors with no usable image — a silent recall ceiling, since the pipeline can never name them and nothing else says why - actors below the positive-pair threshold — not broken, so nothing complains; they just quietly weaken every threshold downstream - near-duplicate references removed, per actor and total - the fitted calibration AND the two distributions behind it That last one is the point. Every threshold in the pipeline is expressed in the probability space this sigmoid defines, so if the distributions overlap heavily the calibration is weak and every downstream decision inherits it — while the gallery still looks fine from the outside. The gallery-derived prior, intra/(intra+inter), is computed and reported but the shipped default of 0.5 is deliberately left alone. The spec records these as disagreeing; now the real value is visible, so the decision can be made on evidence rather than argument. Three tests: a zero-image actor is visible in the report, an under-referenced actor is counted, and the report round-trips through JSON. Suite: 95 cases, 6142 assertions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> TRACES: GR-003 | SR-001
This commit is contained in:
@@ -77,14 +77,48 @@ inline std::function<float(float)> same_person_probability(const GalleryCalibrat
|
||||
return [cal](float similarity) { return cal.probability(similarity); };
|
||||
}
|
||||
|
||||
/// TRACES: GR-003 | SR-001
|
||||
///
|
||||
/// Everything the fit learns about the gallery on its way to two numbers.
|
||||
///
|
||||
/// The fit computes per-actor dedup counts, which actors can supply positive
|
||||
/// pairs at all, and the two similarity distributions the sigmoid is derived
|
||||
/// from — and then returns only (a, b, valid). GR-003 exists because that is the
|
||||
/// evidence for whether the calibration, and so every threshold expressed in its
|
||||
/// probability space (AR-024), rests on anything. Filling this struct costs
|
||||
/// nothing: the values already exist at the point they are copied out.
|
||||
///
|
||||
/// Per-actor vectors are indexed by the actor index used in `flat_actor`.
|
||||
struct GalleryCalibrationStats {
|
||||
int n_actors = 0;
|
||||
int min_embeddings_for_positive = 0;
|
||||
float dedup_sim_threshold = 0.f;
|
||||
|
||||
std::vector<int> distinct_per_actor; // after near-duplicate removal
|
||||
std::vector<int> duplicates_removed_per_actor;
|
||||
std::vector<char> eligible; // 1 = supplies positive pairs
|
||||
|
||||
int hist_bins = 0; // over sim ∈ [-1, 1]
|
||||
std::vector<double> intra_hist;
|
||||
std::vector<double> inter_hist;
|
||||
double n_intra_pairs = 0.0;
|
||||
double n_inter_pairs = 0.0;
|
||||
|
||||
double train_accuracy_pct = 0.0;
|
||||
};
|
||||
|
||||
// Fit a logistic sigmoid to gallery pair similarities.
|
||||
// Positive pairs: same actor, different reference images.
|
||||
// Negative pairs: different actors (all cross-actor embedding pairs).
|
||||
// Class weights balance the (typically skewed) pos/neg ratio.
|
||||
// Requires ≥2 positive pairs and ≥1 negative pair.
|
||||
//
|
||||
// `stats` is optional (GR-003): pass one to receive the dedup, eligibility and
|
||||
// distribution detail the fit would otherwise discard.
|
||||
inline GalleryCalibration calibrate_gallery(
|
||||
const std::vector<Embedding>& flat_emb,
|
||||
const std::vector<int>& flat_actor)
|
||||
const std::vector<int>& flat_actor,
|
||||
GalleryCalibrationStats* stats = nullptr)
|
||||
{
|
||||
constexpr int kMinEmbeddingsForPositive = 5;
|
||||
constexpr float kDedupSimThreshold = 1.f - 1e-7f; // sim above this = duplicate
|
||||
@@ -108,6 +142,21 @@ inline GalleryCalibration calibrate_gallery(
|
||||
std::vector<bool> actor_eligible(n_actors, false);
|
||||
int n_eligible = 0;
|
||||
|
||||
/// TRACES: GR-003 | SR-001
|
||||
// Record what the filter did, per actor, while the counts still exist.
|
||||
if (stats) {
|
||||
*stats = GalleryCalibrationStats{};
|
||||
stats->n_actors = n_actors;
|
||||
stats->min_embeddings_for_positive = kMinEmbeddingsForPositive;
|
||||
stats->dedup_sim_threshold = kDedupSimThreshold;
|
||||
stats->distinct_per_actor.assign(n_actors, 0);
|
||||
stats->duplicates_removed_per_actor.assign(n_actors, 0);
|
||||
stats->eligible.assign(n_actors, 0);
|
||||
stats->hist_bins = kHistBins;
|
||||
stats->intra_hist.assign(kHistBins, 0.0);
|
||||
stats->inter_hist.assign(kHistBins, 0.0);
|
||||
}
|
||||
|
||||
for (int ai = 0; ai < n_actors; ++ai) {
|
||||
std::vector<Embedding> kept;
|
||||
for (const auto& e : by_actor[ai]) {
|
||||
@@ -121,6 +170,12 @@ inline GalleryCalibration calibrate_gallery(
|
||||
actor_eligible[ai] = true;
|
||||
++n_eligible;
|
||||
}
|
||||
if (stats) {
|
||||
stats->distinct_per_actor[ai] = static_cast<int>(kept.size());
|
||||
stats->duplicates_removed_per_actor[ai] =
|
||||
static_cast<int>(by_actor[ai].size() - kept.size());
|
||||
stats->eligible[ai] = actor_eligible[ai] ? 1 : 0;
|
||||
}
|
||||
for (auto& e : kept) {
|
||||
flat_emb_dedup.push_back(e);
|
||||
flat_actor_dedup.push_back(ai);
|
||||
@@ -128,6 +183,14 @@ inline GalleryCalibration calibrate_gallery(
|
||||
}
|
||||
|
||||
const int n = static_cast<int>(flat_emb_dedup.size());
|
||||
|
||||
// Nothing to fit and nothing to multiply. Returning here keeps the report
|
||||
// buildable for a degenerate gallery instead of handing cv::gemm an empty
|
||||
// matrix; the per-actor stats above are already filled and still useful.
|
||||
if (n == 0) {
|
||||
std::cerr << "[calibration] no embeddings — calibration skipped\n";
|
||||
return {};
|
||||
}
|
||||
std::cerr << "[calibration] dedup: " << flat_emb.size() << " -> " << n
|
||||
<< " embeddings (" << n_eligible << "/" << n_actors
|
||||
<< " actors have >= " << kMinEmbeddingsForPositive
|
||||
@@ -228,6 +291,17 @@ inline GalleryCalibration calibrate_gallery(
|
||||
double n_pos = 0.0, n_neg = 0.0;
|
||||
for (int b = 0; b < kHistBins; ++b) { n_pos += pos_hist[b]; n_neg += neg_hist[b]; }
|
||||
|
||||
/// TRACES: GR-003 | SR-001
|
||||
// The two distributions the sigmoid is about to be fitted from. Emitted
|
||||
// whether or not the fit succeeds — a failed fit is exactly the case where
|
||||
// someone needs to see why.
|
||||
if (stats) {
|
||||
stats->intra_hist = pos_hist;
|
||||
stats->inter_hist = neg_hist;
|
||||
stats->n_intra_pairs = n_pos;
|
||||
stats->n_inter_pairs = n_neg;
|
||||
}
|
||||
|
||||
if (n_pos < 2 || n_neg < 1) {
|
||||
std::cerr << "[calibration] insufficient pairs (+" << n_pos
|
||||
<< "/-" << n_neg << ") — calibration skipped\n";
|
||||
@@ -282,6 +356,7 @@ inline GalleryCalibration calibrate_gallery(
|
||||
correct += (sig > 0.5f) ? pos_hist[b] : neg_hist[b];
|
||||
}
|
||||
double acc = 100.0 * correct / total;
|
||||
if (stats) stats->train_accuracy_pct = acc;
|
||||
|
||||
GalleryCalibration cal{a, bias, true};
|
||||
std::cerr << "[calibration] sigmoid fitted:"
|
||||
|
||||
Reference in New Issue
Block a user