diff options
Diffstat (limited to 'cmds/negcap.go')
| -rw-r--r-- | cmds/negcap.go | 34 |
1 files changed, 34 insertions, 0 deletions
diff --git a/cmds/negcap.go b/cmds/negcap.go new file mode 100644 index 0000000..f531b14 --- /dev/null +++ b/cmds/negcap.go @@ -0,0 +1,34 @@ +// 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 +} |
