Skip to content
Back to skills

Per Class Soft F1 Threshold Fitting

ASecurity

Optimize per-class decision thresholds for macro-F1 by replacing the non-differentiable hard threshold with a sigmoid-sharpened soft-F1 surrogate and fitting the per-class threshold vector via least-squares — averaged over multiple random validation splits to suppress overfitting on rare classes

  • 61 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 12, 2026
datapython

Security analysis

A100/100

Scanned September 12, 2026

npx -y skills add wenmin-wu/ds-skills --skill per-class-soft-f1-threshold-fitting --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Per Class Soft F1 Threshold Fitting?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Per Class Soft F1 Threshold Fitting
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/wenmin-wu-per-class-soft-f1-threshold-fitting/badge)](https://www.skillsdirectory.com/skills/wenmin-wu-per-class-soft-f1-threshold-fitting)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: cv-per-class-soft-f1-threshold-fitting
description: Optimize per-class decision thresholds for macro-F1 by replacing the non-differentiable hard threshold with a sigmoid-sharpened soft-F1 surrogate and fitting the per-class threshold vector via least-squares — averaged over multiple random validation splits to suppress overfitting on rare classes
---

## Overview

Picking one global sigmoid threshold (e.g. 0.5) for multi-label classification leaves macro-F1 points on the table because rare classes need lower thresholds and easy classes need higher ones. The naive per-class grid search overfits when validation has only a handful of positives. The trick: replace the hard `(p > th)` step with a sigmoid `σ(d·(p − th))` surrogate, plug into the F1 formula to get a differentiable per-class soft-F1, and use `scipy.optimize.leastsq` with a small L2 penalty to fit the threshold vector. Then average over 10 random train/val splits to denoise. This was the per-class threshold trick that pushed top HPA notebooks from 0.40 to 0.46 LB.

## Quick Start

```python
import numpy as np
from sklearn.model_selection import train_test_split
from scipy import optimize as opt

def sigmoid_np(x): return 1.0 / (1.0 + np.exp(-x))

def F1_soft(preds, targs, th=0.0, d=25.0):
    p = sigmoid_np(d * (preds - th))
    return 2.0 * (p * targs).sum(0) / ((p + targs).sum(0) + 1e-6)

def fit_thresholds(preds, targs, n_classes, wd=1e-5):
    params = np.zeros(n_classes)
    err = lambda p: np.concatenate(
        (F1_soft(preds, targs, p) - 1.0, wd * p), axis=None)
    p, _ = opt.leastsq(err, params)
    return p

# Average across 10 random splits
th = np.zeros(n_classes)
for i in range(10):
    xt, xv, yt, yv = train_test_split(val_pred, val_y, test_size=0.5, random_state=i)
    th += fit_thresholds(xt, yt, n_classes)
th /= 10
```

## Workflow

1. Hold out enough validation examples that every class has ≥ a few positives
2. Run model inference and keep raw sigmoid scores (not hard predictions)
3. Implement `F1_soft` with sharpness `d ≈ 20–30` — too low and the surrogate isn't tight, too high and gradients vanish
4. Wrap in a `leastsq` residual that drives soft-F1 to 1.0, with small L2 weight decay
5. Fit on multiple random subsamples of validation and average — gives a stable per-class threshold vector
6. Apply at inference: `pred_binary = (test_score > th).astype(int)`

## Key Decisions

- **Sigmoid surrogate sharpness `d`**: 25 is the sweet spot — high enough to track hard-F1, low enough to keep gradients non-zero.
- **Why `leastsq` not gradient descent**: F1 surface has many flats; least-squares with small wd converges in milliseconds and is reproducible.
- **Average across splits, don't fit once**: rare classes overfit a single split easily; 10 splits cuts variance ~3x.
- **Initialize at 0**: the sigmoid centers naturally; non-zero init biases towards a global threshold.
- **Don't fit on training data**: the model is overconfident on train and the thresholds will be too high.
- **Generalizes to any per-instance multi-label problem**: NLP entity tagging, audio multi-tag, recsys multi-genre.

## References

- [Pretrained ResNet34 with RGBY (0.460 public LB)](https://www.kaggle.com/code/iafoss/pretrained-resnet34-with-rgby-0-460-public-lb)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.