// Per-feed negative ceiling, shared by `export` (enforced while sampling) // and `compact-labels` (enforced against existing history). // // Why: negatives are sampled from the read+unstarred pool, whose size is // dominated by high-volume feeds. Without a ceiling, a handful of firehoses // own most of the negative class, and the model learns "wire headline" as // its main axis instead of "headline samsc liked". The ceiling is on each // feed's *total* negatives (existing history + this run's additions), not // just the run's sample: a per-run cap would only slow domination, not // bound it. package cmds // negFeedCapFloor is the absolute minimum ceiling. It keeps a young, // still-small negative pool from capping feeds at a handful of rows: // early training needs volume more than it needs balance. const negFeedCapFloor = 50 // feedNegCap returns the maximum negatives any single feed may hold in the // pool: pct% of the target (negRatio × posTotal), floored at negFeedCapFloor // and never above the target itself. pct <= 0 means uncapped (0). func feedNegCap(pct, negRatio, posTotal int) int { if pct <= 0 { return 0 } target := negRatio * posTotal cap := pct * target / 100 if cap < negFeedCapFloor { cap = negFeedCapFloor } if cap > target { cap = target } return cap }