Switch to unified view

a b/yolov5/utils/autoanchor.py
1
# YOLOv5 🚀 by Ultralytics, GPL-3.0 license
2
"""
3
Auto-anchor utils
4
"""
5
6
import random
7
8
import numpy as np
9
import torch
10
import yaml
11
from tqdm import tqdm
12
13
from utils.general import LOGGER, colorstr, emojis
14
15
PREFIX = colorstr('AutoAnchor: ')
16
17
18
def check_anchor_order(m):
19
    # Check anchor order against stride order for YOLOv5 Detect() module m, and correct if necessary
20
    a = m.anchors.prod(-1).view(-1)  # anchor area
21
    da = a[-1] - a[0]  # delta a
22
    ds = m.stride[-1] - m.stride[0]  # delta s
23
    if da.sign() != ds.sign():  # same order
24
        LOGGER.info(f'{PREFIX}Reversing anchor order')
25
        m.anchors[:] = m.anchors.flip(0)
26
27
28
def check_anchors(dataset, model, thr=4.0, imgsz=640):
29
    # Check anchor fit to data, recompute if necessary
30
    m = model.module.model[-1] if hasattr(model, 'module') else model.model[-1]  # Detect()
31
    shapes = imgsz * dataset.shapes / dataset.shapes.max(1, keepdims=True)
32
    scale = np.random.uniform(0.9, 1.1, size=(shapes.shape[0], 1))  # augment scale
33
    wh = torch.tensor(np.concatenate([l[:, 3:5] * s for s, l in zip(shapes * scale, dataset.labels)])).float()  # wh
34
35
    def metric(k):  # compute metric
36
        r = wh[:, None] / k[None]
37
        x = torch.min(r, 1 / r).min(2)[0]  # ratio metric
38
        best = x.max(1)[0]  # best_x
39
        aat = (x > 1 / thr).float().sum(1).mean()  # anchors above threshold
40
        bpr = (best > 1 / thr).float().mean()  # best possible recall
41
        return bpr, aat
42
43
    anchors = m.anchors.clone() * m.stride.to(m.anchors.device).view(-1, 1, 1)  # current anchors
44
    bpr, aat = metric(anchors.cpu().view(-1, 2))
45
    s = f'\n{PREFIX}{aat:.2f} anchors/target, {bpr:.3f} Best Possible Recall (BPR). '
46
    if bpr > 0.98:  # threshold to recompute
47
        LOGGER.info(emojis(f'{s}Current anchors are a good fit to dataset ✅'))
48
    else:
49
        LOGGER.info(emojis(f'{s}Anchors are a poor fit to dataset ⚠️, attempting to improve...'))
50
        na = m.anchors.numel() // 2  # number of anchors
51
        try:
52
            anchors = kmean_anchors(dataset, n=na, img_size=imgsz, thr=thr, gen=1000, verbose=False)
53
        except Exception as e:
54
            LOGGER.info(f'{PREFIX}ERROR: {e}')
55
        new_bpr = metric(anchors)[0]
56
        if new_bpr > bpr:  # replace anchors
57
            anchors = torch.tensor(anchors, device=m.anchors.device).type_as(m.anchors)
58
            m.anchors[:] = anchors.clone().view_as(m.anchors) / m.stride.to(m.anchors.device).view(-1, 1, 1)  # loss
59
            check_anchor_order(m)
60
            LOGGER.info(f'{PREFIX}New anchors saved to model. Update model *.yaml to use these anchors in the future.')
61
        else:
62
            LOGGER.info(f'{PREFIX}Original anchors better than new anchors. Proceeding with original anchors.')
63
64
65
def kmean_anchors(dataset='./data/coco128.yaml', n=9, img_size=640, thr=4.0, gen=1000, verbose=True):
66
    """ Creates kmeans-evolved anchors from training dataset
67
68
        Arguments:
69
            dataset: path to data.yaml, or a loaded dataset
70
            n: number of anchors
71
            img_size: image size used for training
72
            thr: anchor-label wh ratio threshold hyperparameter hyp['anchor_t'] used for training, default=4.0
73
            gen: generations to evolve anchors using genetic algorithm
74
            verbose: print all results
75
76
        Return:
77
            k: kmeans evolved anchors
78
79
        Usage:
80
            from utils.autoanchor import *; _ = kmean_anchors()
81
    """
82
    from scipy.cluster.vq import kmeans
83
84
    thr = 1 / thr
85
86
    def metric(k, wh):  # compute metrics
87
        r = wh[:, None] / k[None]
88
        x = torch.min(r, 1 / r).min(2)[0]  # ratio metric
89
        # x = wh_iou(wh, torch.tensor(k))  # iou metric
90
        return x, x.max(1)[0]  # x, best_x
91
92
    def anchor_fitness(k):  # mutation fitness
93
        _, best = metric(torch.tensor(k, dtype=torch.float32), wh)
94
        return (best * (best > thr).float()).mean()  # fitness
95
96
    def print_results(k, verbose=True):
97
        k = k[np.argsort(k.prod(1))]  # sort small to large
98
        x, best = metric(k, wh0)
99
        bpr, aat = (best > thr).float().mean(), (x > thr).float().mean() * n  # best possible recall, anch > thr
100
        s = f'{PREFIX}thr={thr:.2f}: {bpr:.4f} best possible recall, {aat:.2f} anchors past thr\n' \
101
            f'{PREFIX}n={n}, img_size={img_size}, metric_all={x.mean():.3f}/{best.mean():.3f}-mean/best, ' \
102
            f'past_thr={x[x > thr].mean():.3f}-mean: '
103
        for i, x in enumerate(k):
104
            s += '%i,%i, ' % (round(x[0]), round(x[1]))
105
        if verbose:
106
            LOGGER.info(s[:-2])
107
        return k
108
109
    if isinstance(dataset, str):  # *.yaml file
110
        with open(dataset, errors='ignore') as f:
111
            data_dict = yaml.safe_load(f)  # model dict
112
        from utils.datasets import LoadImagesAndLabels
113
        dataset = LoadImagesAndLabels(data_dict['train'], augment=True, rect=True)
114
115
    # Get label wh
116
    shapes = img_size * dataset.shapes / dataset.shapes.max(1, keepdims=True)
117
    wh0 = np.concatenate([l[:, 3:5] * s for s, l in zip(shapes, dataset.labels)])  # wh
118
119
    # Filter
120
    i = (wh0 < 3.0).any(1).sum()
121
    if i:
122
        LOGGER.info(f'{PREFIX}WARNING: Extremely small objects found. {i} of {len(wh0)} labels are < 3 pixels in size.')
123
    wh = wh0[(wh0 >= 2.0).any(1)]  # filter > 2 pixels
124
    # wh = wh * (np.random.rand(wh.shape[0], 1) * 0.9 + 0.1)  # multiply by random scale 0-1
125
126
    # Kmeans calculation
127
    LOGGER.info(f'{PREFIX}Running kmeans for {n} anchors on {len(wh)} points...')
128
    s = wh.std(0)  # sigmas for whitening
129
    k, dist = kmeans(wh / s, n, iter=30)  # points, mean distance
130
    assert len(k) == n, f'{PREFIX}ERROR: scipy.cluster.vq.kmeans requested {n} points but returned only {len(k)}'
131
    k *= s
132
    wh = torch.tensor(wh, dtype=torch.float32)  # filtered
133
    wh0 = torch.tensor(wh0, dtype=torch.float32)  # unfiltered
134
    k = print_results(k, verbose=False)
135
136
    # Plot
137
    # k, d = [None] * 20, [None] * 20
138
    # for i in tqdm(range(1, 21)):
139
    #     k[i-1], d[i-1] = kmeans(wh / s, i)  # points, mean distance
140
    # fig, ax = plt.subplots(1, 2, figsize=(14, 7), tight_layout=True)
141
    # ax = ax.ravel()
142
    # ax[0].plot(np.arange(1, 21), np.array(d) ** 2, marker='.')
143
    # fig, ax = plt.subplots(1, 2, figsize=(14, 7))  # plot wh
144
    # ax[0].hist(wh[wh[:, 0]<100, 0],400)
145
    # ax[1].hist(wh[wh[:, 1]<100, 1],400)
146
    # fig.savefig('wh.png', dpi=200)
147
148
    # Evolve
149
    npr = np.random
150
    f, sh, mp, s = anchor_fitness(k), k.shape, 0.9, 0.1  # fitness, generations, mutation prob, sigma
151
    pbar = tqdm(range(gen), desc=f'{PREFIX}Evolving anchors with Genetic Algorithm:')  # progress bar
152
    for _ in pbar:
153
        v = np.ones(sh)
154
        while (v == 1).all():  # mutate until a change occurs (prevent duplicates)
155
            v = ((npr.random(sh) < mp) * random.random() * npr.randn(*sh) * s + 1).clip(0.3, 3.0)
156
        kg = (k.copy() * v).clip(min=2.0)
157
        fg = anchor_fitness(kg)
158
        if fg > f:
159
            f, k = fg, kg.copy()
160
            pbar.desc = f'{PREFIX}Evolving anchors with Genetic Algorithm: fitness = {f:.4f}'
161
            if verbose:
162
                print_results(k, verbose)
163
164
    return print_results(k)