CharlesCNorton
Image-level person classification on EUPE-ViT-B features with no free parameters
e8b8483 | """Choose the two dim sets on train2017 and write rules.json. | |
| python choose.py | |
| The decision is sum(feat[pos]) > sum(feat[neg]), evaluated at zero. A threshold | |
| would absorb the offset between the two sums; choosing the sets so the offset is | |
| already zero removes it instead, which is why the rule carries no free parameter. | |
| Dims are added greedily, alternating sides, each step taking whichever remaining | |
| dim most improves F1 with the boundary pinned at zero. Selection reads only | |
| train2017. Nothing here touches val2017. | |
| """ | |
| import argparse | |
| from pathlib import Path | |
| import torch | |
| from common import COCO_ROOT, prf1, write_artifact | |
| from common.cached import load_pooled | |
| from common.pools import TRAIN2017 | |
| HERE = Path(__file__).resolve().parent | |
| def greedy(X, y, k, candidates): | |
| """Grow pos and neg sets to k each, scoring only at threshold zero.""" | |
| pos, neg = [], [] | |
| cur = torch.zeros(X.shape[0]) | |
| for step in range(2 * k): | |
| side, sign = (pos, 1.0) if step % 2 == 0 else (neg, -1.0) | |
| used = set(pos) | set(neg) | |
| best_f1, best_d = -1.0, None | |
| for d in candidates: | |
| if d in used: | |
| continue | |
| f1 = prf1(cur + sign * X[:, d] > 0, y).f1 | |
| if f1 > best_f1: | |
| best_f1, best_d = f1, d | |
| side.append(int(best_d)) | |
| cur = cur + sign * X[:, best_d] | |
| return pos, neg, prf1(cur > 0, y).f1 | |
| def main(): | |
| ap = argparse.ArgumentParser(description=__doc__) | |
| ap.add_argument('--cache', type=Path, default=None) | |
| ap.add_argument('--sizes', type=int, nargs='+', | |
| default=[1, 2, 3, 4, 6, 8, 10, 20]) | |
| ap.add_argument('--candidates', type=int, default=192) | |
| ap.add_argument('--out', type=Path, default=HERE / 'rules.json') | |
| args = ap.parse_args() | |
| cache = args.cache or COCO_ROOT / 'pooled_train2017' | |
| X, y = load_pooled(cache, 'train2017') | |
| print(f'[train] {X.shape[0]} images, person rate {y.float().mean():.3f}', | |
| flush=True) | |
| # Restrict the search to dims with real class separation, for tractability. | |
| sep = (X[y].mean(0) - X[~y].mean(0)).abs() | |
| candidates = torch.topk(sep, args.candidates).indices.tolist() | |
| rules = {} | |
| for k in args.sizes: | |
| pos, neg, f1 = greedy(X, y, k, candidates) | |
| rules[f'd{2 * k}'] = {'pos_dims': pos, 'neg_dims': neg, | |
| 'n_dims': 2 * k, 'free_parameters': 0, | |
| 'F1_train': round(f1, 4)} | |
| print(f' d{2 * k:<3} F1 {f1:.4f} {pos} > {neg}', flush=True) | |
| write_artifact(args.out, { | |
| 'candidates': args.candidates, | |
| 'sizes': [2 * k for k in args.sizes], | |
| 'rules': rules, | |
| }, generator='choose.py', | |
| pool_info={'pool': TRAIN2017.name, 'split': TRAIN2017.split, | |
| 'n_images': int(X.shape[0]), | |
| 'positive_rate': round(y.float().mean().item(), 4), | |
| 'selection': TRAIN2017.selection}, | |
| decision='sum(feat[pos_dims]) > sum(feat[neg_dims])', | |
| search='greedy, alternating sides, boundary pinned at zero') | |
| print(f'[done] wrote {args.out}', flush=True) | |
| if __name__ == '__main__': | |
| main() | |