def _choose_action(file_infos): import kwarray file_infos = kwarray.shuffle(file_infos, rng=0) n_keep = (len(file_infos) // factor) + 1 for info in file_infos[:n_keep]: info['action'] = 'keep' for info in file_infos[n_keep:]: info['action'] = 'delete'
def the_core_dataset_backend(): import kwcoco dset = kwcoco.CocoDataset.demo('shapes2') # Make data slightly tider for display for ann in dset.dataset['annotations']: ann.pop('segmentation', None) ann.pop('keypoints', None) ann.pop('area', None) for cat in dset.dataset['categories']: cat.pop('keypoints', None) for img in dset.dataset['images']: img.pop('channels', None) dset.reroot(dirname(dset.fpath), absolute=False) import kwarray dset.remove_annotations(kwarray.shuffle(list(dset.anns.keys()))[10:]) print('dset.dataset = {}'.format(ub.repr2(dset.dataset, nl=2)))
def _coerce_datasets(config): import netharn as nh import ndsampler import numpy as np from torchvision import transforms coco_datasets = nh.api.Datasets.coerce(config) print('coco_datasets = {}'.format(ub.repr2(coco_datasets, nl=1))) for tag, dset in coco_datasets.items(): dset._build_hashid(hash_pixels=False) workdir = ub.ensuredir(ub.expandpath(config['workdir'])) samplers = { tag: ndsampler.CocoSampler(dset, workdir=workdir, backend=config['sampler_backend']) for tag, dset in coco_datasets.items() } for tag, sampler in ub.ProgIter(list(samplers.items()), desc='prepare frames'): sampler.frames.prepare(workers=config['workers']) # TODO: basic ndsampler torch dataset, likely has to support the transforms # API, bleh. transform = transforms.Compose([ transforms.Resize(config['input_dims']), transforms.CenterCrop(config['input_dims']), transforms.ToTensor(), transforms.Lambda(lambda x: x.mul(255)) ]) torch_datasets = { key: SamplerDataset( sapmler, transform=transform, # input_dims=config['input_dims'], # augmenter=config['augmenter'] if key == 'train' else None, ) for key, sapmler in samplers.items() } # self = torch_dset = torch_datasets['train'] if config['normalize_inputs']: # Get stats on the dataset (todo: turn off augmentation for this) import kwarray _dset = torch_datasets['train'] stats_idxs = kwarray.shuffle(np.arange(len(_dset)), rng=0)[0:min(1000, len(_dset))] stats_subset = torch.utils.data.Subset(_dset, stats_idxs) cacher = ub.Cacher('dset_mean', cfgstr=_dset.input_id + 'v3') input_stats = cacher.tryload() from netharn.data.channel_spec import ChannelSpec channels = ChannelSpec.coerce(config['channels']) if input_stats is None: # Use parallel workers to load data faster from netharn.data.data_containers import container_collate from functools import partial collate_fn = partial(container_collate, num_devices=1) loader = torch.utils.data.DataLoader( stats_subset, collate_fn=collate_fn, num_workers=config['workers'], shuffle=True, batch_size=config['batch_size']) # Track moving average of each fused channel stream channel_stats = {key: nh.util.RunningStats() for key in channels.keys()} assert len(channel_stats) == 1, ( 'only support one fused stream for now') for batch in ub.ProgIter(loader, desc='estimate mean/std'): if isinstance(batch, (tuple, list)): inputs = {'rgb': batch[0]} # make assumption else: inputs = batch['inputs'] for key, val in inputs.items(): try: for part in val.numpy(): channel_stats[key].update(part) except ValueError: # final batch broadcast error pass perchan_input_stats = {} for key, running in channel_stats.items(): running = ub.peek(channel_stats.values()) perchan_stats = running.simple(axis=(1, 2)) perchan_input_stats[key] = { 'std': perchan_stats['mean'].round(3), 'mean': perchan_stats['std'].round(3), } input_stats = ub.peek(perchan_input_stats.values()) cacher.save(input_stats) else: input_stats = {} torch_loaders = { tag: dset.make_loader( batch_size=config['batch_size'], num_batches=config['num_batches'], num_workers=config['workers'], shuffle=(tag == 'train'), balance=(config['balance'] if tag == 'train' else None), pin_memory=True) for tag, dset in torch_datasets.items() } dataset_info = { 'torch_datasets': torch_datasets, 'torch_loaders': torch_loaders, 'input_stats': input_stats } return dataset_info
def setup_harn(cmdline=True, **kw): """ Ignore: >>> from object_detection import * # NOQA >>> cmdline = False >>> kw = { >>> 'train_dataset': '~/data/VOC/voc-trainval.mscoco.json', >>> 'vali_dataset': '~/data/VOC/voc-test-2007.mscoco.json', >>> } >>> harn = setup_harn(**kw) """ import ndsampler from ndsampler import coerce_data # Seed other global rngs just in case something uses them under the hood kwarray.seed_global(1129989262, offset=1797315558) config = DetectFitConfig(default=kw, cmdline=cmdline) nh.configure_hacks(config) # fix opencv bugs ub.ensuredir(config['workdir']) # Load ndsampler.CocoDataset objects from info in the config subsets = coerce_data.coerce_datasets(config) samplers = {} for tag, subset in subsets.items(): print('subset = {!r}'.format(subset)) sampler = ndsampler.CocoSampler(subset, workdir=config['workdir']) samplers[tag] = sampler torch_datasets = { tag: DetectDataset( sampler, input_dims=config['input_dims'], augment=config['augment'] if (tag == 'train') else False, ) for tag, sampler in samplers.items() } print('make loaders') loaders_ = { tag: torch.utils.data.DataLoader(dset, batch_size=config['batch_size'], num_workers=config['workers'], shuffle=(tag == 'train'), collate_fn=nh.data.collate.padded_collate, pin_memory=True) for tag, dset in torch_datasets.items() } # for x in ub.ProgIter(loaders_['train']): # pass if config['normalize_inputs']: # Get stats on the dataset (todo: turn off augmentation for this) _dset = torch_datasets['train'] stats_idxs = kwarray.shuffle(np.arange(len(_dset)), rng=0)[0:min(1000, len(_dset))] stats_subset = torch.utils.data.Subset(_dset, stats_idxs) cacher = ub.Cacher('dset_mean', cfgstr=_dset.input_id + 'v2') input_stats = cacher.tryload() if input_stats is None: # Use parallel workers to load data faster loader = torch.utils.data.DataLoader( stats_subset, collate_fn=nh.data.collate.padded_collate, num_workers=config['workers'], shuffle=True, batch_size=config['batch_size']) # Track moving average running = nh.util.RunningStats() for batch in ub.ProgIter(loader, desc='estimate mean/std'): try: running.update(batch['im'].numpy()) except ValueError: # final batch broadcast error pass input_stats = { 'std': running.simple(axis=None)['mean'].round(3), 'mean': running.simple(axis=None)['std'].round(3), } cacher.save(input_stats) else: input_stats = None print('input_stats = {!r}'.format(input_stats)) initializer_ = nh.Initializer.coerce(config, leftover='kaiming_normal') print('initializer_ = {!r}'.format(initializer_)) arch = config['arch'] if arch == 'yolo2': if False: dset = samplers['train'].dset print('dset = {!r}'.format(dset)) # anchors = yolo2.find_anchors(dset) anchors = np.array([(1.3221, 1.73145), (3.19275, 4.00944), (5.05587, 8.09892), (9.47112, 4.84053), (11.2364, 10.0071)]) classes = samplers['train'].classes model_ = (yolo2.Yolo2, { 'classes': classes, 'anchors': anchors, 'conf_thresh': 0.001, 'nms_thresh': 0.5 if not ub.argflag('--eav') else 0.4 }) model = model_[0](**model_[1]) model._initkw = model_[1] criterion_ = ( yolo2.YoloLoss, { 'coder': model.coder, 'seen': 0, 'coord_scale': 1.0, 'noobject_scale': 1.0, 'object_scale': 5.0, 'class_scale': 1.0, 'thresh': 0.6, # iou_thresh # 'seen_thresh': 12800, }) else: raise KeyError(arch) scheduler_ = nh.Scheduler.coerce(config) print('scheduler_ = {!r}'.format(scheduler_)) optimizer_ = nh.Optimizer.coerce(config) print('optimizer_ = {!r}'.format(optimizer_)) dynamics_ = nh.Dynamics.coerce(config) print('dynamics_ = {!r}'.format(dynamics_)) xpu = nh.XPU.coerce(config['xpu']) print('xpu = {!r}'.format(xpu)) import sys hyper = nh.HyperParams( **{ 'nice': config['nice'], 'workdir': config['workdir'], 'datasets': torch_datasets, 'loaders': loaders_, 'xpu': xpu, 'model': model, 'criterion': criterion_, 'initializer': initializer_, 'optimizer': optimizer_, 'dynamics': dynamics_, # 'optimizer': (torch.optim.SGD, { # 'lr': lr_step_points[0], # 'momentum': 0.9, # 'dampening': 0, # # multiplying by batch size was one of those unpublished details # 'weight_decay': decay * simulated_bsize, # }), 'scheduler': scheduler_, 'monitor': ( nh.Monitor, { 'minimize': ['loss'], # 'maximize': ['mAP'], 'patience': config['patience'], 'max_epoch': config['max_epoch'], 'smoothing': .6, }), 'other': { # Other params are not used internally, so you are free to set any # extra params specific to your algorithm, and still have them # logged in the hyperparam structure. For YOLO this is `ovthresh`. 'batch_size': config['batch_size'], 'nice': config['nice'], 'ovthresh': config['ovthresh'], # used in mAP computation }, 'extra': { 'config': ub.repr2(config.asdict()), 'argv': sys.argv, } }) print('hyper = {!r}'.format(hyper)) print('make harn') harn = DetectHarn(hyper=hyper) harn.preferences.update({ 'num_keep': 2, 'keep_freq': 30, 'export_modules': ['netharn'], # TODO 'prog_backend': 'progiter', # alternative: 'tqdm' 'keyboard_debug': True, }) harn.intervals.update({ 'log_iter_train': 50, }) harn.fit_config = config print('harn = {!r}'.format(harn)) print('samplers = {!r}'.format(samplers)) return harn
def mine_negatives(self, dvecs, labels, num=1, mode='hardest', eps=1e-9): """ triplets = are a selection of anchor, positive, and negative annots chosen to be the hardest for each annotation (with a valid pos and neg partner) in the batch. Args: dvecs (Tensor): descriptor vectors for each item labels (Tensor): id-label for each descriptor vector num (int, default=1): number of negatives per positive combination mode (str, default='hardest'): method for selecting negatives eps (float, default=1e9): distance threshold for near duplicates Returns: Dict: info: containing neg_dists, pos_dists, triples, and dist CommandLine: xdoctest -m netharn.criterions.triplet TripletLoss.mine_negatives:1 --profile Example: >>> from netharn.criterions.triplet import * >>> dvecs = torch.FloatTensor([ ... # Individual 1 ... [1.0, 0.0, 0.0, ], ... [0.9, 0.1, 0.0, ], # Looks like 2 [1] ... # Individual 2 ... [0.0, 1.0, 0.0, ], ... [0.0, 0.9, 0.1, ], # Looks like 3 [3] ... # Individual 3 ... [0.0, 0.0, 1.0, ], ... [0.1, 0.0, 0.9, ], # Looks like 1 [5] >>> ]) >>> import itertools as it >>> labels = torch.LongTensor([0, 0, 1, 1, 2, 2]) >>> num = 1 >>> info = TripletLoss().mine_negatives(dvecs, labels, num) >>> print('info = {!r}'.format(info)) >>> assert torch.all(info['pos_dists'] < info['neg_dists']) Example: >>> # xdoxctest: +SKIP >>> import itertools as it >>> p = 3 >>> k = 10 >>> d = p >>> mode = 'consistent' >>> mode = 'hardest' >>> for p, k in it.product(range(3, 13), range(2, 13)): >>> d = p >>> def make_individual_dvecs(i): >>> vecs = torch.zeros((k, d)) >>> vecs[:, i] = torch.linspace(0.9, 1.0, k) >>> return vecs >>> dvecs = torch.cat([make_individual_dvecs(i) for i in range(p)], dim=0) >>> labels = torch.LongTensor(np.hstack([[i] * k for i in range(p)])) >>> num = 1 >>> info = TripletLoss().mine_negatives(dvecs, labels, num, mode=mode) >>> if mode.startswith('hard'): >>> assert torch.all(info['pos_dists'] < info['neg_dists']) >>> base = k >>> for a, p, n in info['triples']: >>> x = a // base >>> y = p // base >>> z = n // base >>> assert x == y, str([a, p, n]) >>> assert x != z, str([a, p, n]) """ import kwarray dist = all_pairwise_distances(dvecs, squared=True, approx=True) with torch.no_grad(): labels_ = labels.numpy() symmetric = False pos_adjm = labels_to_adjacency_matrix(labels_, symmetric=symmetric, diagonal=False) if symmetric: neg_adjm = 1 - pos_adjm else: neg_adjm = 1 - pos_adjm - pos_adjm.T np.fill_diagonal(neg_adjm, 0) # ignore near duplicates dist_ = dist.data.cpu().numpy() is_near_dup = np.where(dist_ < eps) neg_adjm[is_near_dup] = 0 pos_adjm[is_near_dup] = 0 # Filter out any anchor row that does not have both a positive and # a negative match flags = np.any(pos_adjm, axis=1) & np.any(neg_adjm, axis=1) anchors_idxs = np.where(flags)[0] pos_adjm_ = pos_adjm[flags].astype(bool) neg_adjm_ = neg_adjm[flags].astype(bool) if mode == 'hardest': # Order each anchors positives and negatives by increasing distance sortx_ = dist_[flags].argsort(axis=1) pos_cands_list = [x[m[x]] for x, m in zip(sortx_, pos_adjm_)] neg_cands_list = [x[n[x]] for x, n in zip(sortx_, neg_adjm_)] triples = [] backup = [] _iter = zip(anchors_idxs, pos_cands_list, neg_cands_list) for (anchor_idx, pos_cands, neg_cands) in _iter: # Take `num` hardest negative pairs for each positive pair num_ = min(len(neg_cands), len(pos_cands), num) pos_idxs = pos_cands neg_idxs = neg_cands[:num_] anchor_dists = dist_[anchor_idx] if True: pos_dists = anchor_dists[pos_idxs] neg_dists = anchor_dists[neg_idxs] # Ignore any triple that satisfies the margin # constraint losses = pos_dists[:, None] - neg_dists[None, :] + self.margin ilocs, jlocs = np.where(losses > 0) if len(ilocs) > 0: valid_pos_idxs = pos_idxs[ilocs].tolist() valid_neg_idxs = neg_idxs[jlocs].tolist() for pos_idx, neg_idx in zip(valid_pos_idxs, valid_neg_idxs): triples.append((anchor_idx, pos_idx, neg_idx)) elif len(triples) == 0: # Take everything because we might need a backup for pos_idx, neg_idx in it.product(pos_idxs, neg_idxs): backup.append((anchor_idx, pos_idx, neg_idx)) else: for pos_idx, neg_idx in it.product(pos_idxs, neg_idxs): # Only take items that will contribute positive loss d_ap = anchor_dists[pos_idx] d_an = anchor_dists[neg_idx] loss = d_ap - d_an + self.margin if loss > 0: triples.append((anchor_idx, pos_idx, neg_idx)) elif len(triples) == 0: backup.append((anchor_idx, pos_idx, neg_idx)) elif mode == 'moderate': pos_cands_list = [np.where(m)[0].tolist() for m in pos_adjm_] neg_cands_list = [np.where(n)[0].tolist() for n in neg_adjm_] triples = [] backup = [] _iter = zip(anchors_idxs, pos_cands_list, neg_cands_list) for (anchor_idx, pos_cands, neg_cands) in _iter: # Take `num` moderate negative pairs for each positive pair # Only take items that will contribute positive loss # but try not to take any that are too hard. anchor_dists = dist_[anchor_idx] neg_dists = anchor_dists[neg_cands] for pos_idx in pos_cands: pos_dist = anchor_dists[pos_idx] losses = pos_dist - neg_dists + self.margin # valid_negs = np.where((losses < margin) & (losses > 0))[0] valid_negs = np.where(losses > 0)[0] if len(valid_negs): neg_idx = neg_cands[np.random.choice(valid_negs)] triples.append((anchor_idx, pos_idx, neg_idx)) elif len(triples) == 0: # We try to always return valid triples so, create # a backup set in case we cant find any valid # candidates neg_idx = neg_cands[0] backup.append((anchor_idx, pos_idx, neg_idx)) elif mode == 'consistent': # Choose the same triples every time rng = kwarray.ensure_rng(0) pos_cands_list = [kwarray.shuffle(np.where(m)[0], rng=rng) for m in pos_adjm_] neg_cands_list = [kwarray.shuffle(np.where(n)[0], rng=rng) for n in neg_adjm_] triples = [] _iter = zip(anchors_idxs, pos_cands_list, neg_cands_list) for (anchor_idx, pos_cands, neg_cands) in _iter: num_ = min(len(neg_cands), len(pos_cands), num) pos_idxs = pos_cands for pos_idx in pos_idxs: neg_idx = rng.choice(neg_cands) triples.append((anchor_idx, pos_idx, neg_idx)) rng.shuffle(triples) else: raise KeyError(mode) if len(triples) == 0: triples = backup if len(backup) == 0: raise RuntimeError('unable to mine triples') triples = np.array(triples) A, P, N = triples.T if 0 and __debug__: if labels is not None: for a, p, n in triples: na_ = labels[a] np_ = labels[p] nn_ = labels[n] assert na_ == np_ assert np_ != nn_ # Note these distances are approximate distances, but they should be # good enough to backprop through (if not see alternate commented code). pos_dists = dist[A, P] neg_dists = dist[A, N] # pos_dists = (dvecs[A] - dvecs[P]).pow(2).sum(1) # neg_dists = (dvecs[A] - dvecs[N]).pow(2).sum(1) info = { 'pos_dists': pos_dists, 'neg_dists': neg_dists, 'triples': triples, 'dist': dist, } return info
def setup_harn(cmdline=True, **kw): """ CommandLine: xdoctest -m netharn.examples.segmentation setup_harn Example: >>> # xdoctest: +REQUIRES(--slow) >>> kw = {'workers': 0, 'xpu': 'cpu', 'batch_size': 2} >>> cmdline = False >>> # Just sets up the harness, does not do any heavy lifting >>> harn = setup_harn(cmdline=cmdline, **kw) >>> # >>> harn.initialize() >>> # >>> batch = harn._demo_batch(tag='train') >>> epoch_metrics = harn._demo_epoch(tag='vali', max_iter=2) """ import sys import ndsampler import kwarray # kwarray.seed_global(2108744082) config = SegmentationConfig(default=kw) config.load(cmdline=cmdline) nh.configure_hacks(config) # fix opencv bugs coco_datasets = nh.api.Datasets.coerce(config) print('coco_datasets = {}'.format(ub.repr2(coco_datasets))) for tag, dset in coco_datasets.items(): dset._build_hashid(hash_pixels=False) workdir = ub.ensuredir(ub.expandpath(config['workdir'])) samplers = { tag: ndsampler.CocoSampler(dset, workdir=workdir, backend=config['backend']) for tag, dset in coco_datasets.items() } for tag, sampler in ub.ProgIter(list(samplers.items()), desc='prepare frames'): try: sampler.frames.prepare(workers=config['workers']) except AttributeError: pass torch_datasets = { tag: SegmentationDataset( sampler, config['input_dims'], input_overlap=((tag == 'train') and config['input_overlap']), augmenter=((tag == 'train') and config['augmenter']), ) for tag, sampler in samplers.items() } torch_loaders = { tag: torch_data.DataLoader(dset, batch_size=config['batch_size'], num_workers=config['workers'], shuffle=(tag == 'train'), drop_last=True, pin_memory=True) for tag, dset in torch_datasets.items() } if config['class_weights']: mode = config['class_weights'] dset = torch_datasets['train'] class_weights = _precompute_class_weights(dset, mode=mode, workers=config['workers']) class_weights = torch.FloatTensor(class_weights) class_weights[dset.classes.index('background')] = 0 else: class_weights = None if config['normalize_inputs']: stats_dset = torch_datasets['train'] stats_idxs = kwarray.shuffle(np.arange(len(stats_dset)), rng=0)[0:min(1000, len(stats_dset))] stats_subset = torch.utils.data.Subset(stats_dset, stats_idxs) cacher = ub.Cacher('dset_mean', cfgstr=stats_dset.input_id + 'v3') input_stats = cacher.tryload() if input_stats is None: loader = torch.utils.data.DataLoader( stats_subset, num_workers=config['workers'], shuffle=True, batch_size=config['batch_size']) running = nh.util.RunningStats() for batch in ub.ProgIter(loader, desc='estimate mean/std'): try: running.update(batch['im'].numpy()) except ValueError: # final batch broadcast error pass input_stats = { 'std': running.simple(axis=None)['mean'].round(3), 'mean': running.simple(axis=None)['std'].round(3), } cacher.save(input_stats) else: input_stats = {} print('input_stats = {!r}'.format(input_stats)) # TODO: infer numbr of channels model_ = (SegmentationModel, { 'arch': config['arch'], 'input_stats': input_stats, 'classes': torch_datasets['train'].classes.__json__(), 'in_channels': 3, }) initializer_ = nh.Initializer.coerce(config) # if config['init'] == 'cls': # initializer_ = model_[0]._initializer_cls() # Create hyperparameters hyper = nh.HyperParams( nice=config['nice'], workdir=config['workdir'], xpu=nh.XPU.coerce(config['xpu']), datasets=torch_datasets, loaders=torch_loaders, model=model_, initializer=initializer_, scheduler=nh.Scheduler.coerce(config), optimizer=nh.Optimizer.coerce(config), dynamics=nh.Dynamics.coerce(config), criterion=( nh.criterions.FocalLoss, { 'focus': config['focus'], 'weight': class_weights, # 'reduction': 'none', }), monitor=(nh.Monitor, { 'minimize': ['loss'], 'patience': config['patience'], 'max_epoch': config['max_epoch'], 'smoothing': .6, }), other={ 'batch_size': config['batch_size'], }, extra={ 'argv': sys.argv, 'config': ub.repr2(config.asdict()), }) # Create harness harn = SegmentationHarn(hyper=hyper) harn.classes = torch_datasets['train'].classes harn.preferences.update({ 'num_keep': 2, 'keyboard_debug': True, # 'export_modules': ['netharn'], }) harn.intervals.update({ 'vali': 1, 'test': 10, }) harn.script_config = config return harn
def refresh(self): import kwarray self._pos = 0 if self.shuffle: self.indices = kwarray.shuffle(self.indices, rng=self.rng)
def setup_harn(cmdline=True, **kw): """ This creates the "The Classification Harness" (i.e. core ClfHarn object). This is where we programmatically connect our program arguments with the netharn HyperParameter standards. We are using :module:`scriptconfig` to capture these, but you could use click / argparse / etc. This function has the responsibility of creating our torch datasets, lazy computing input statistics, specifying our model architecture, schedule, initialization, optimizer, dynamics, XPU etc. These can usually be coerced using netharn API helpers and a "standardized" config dict. See the function code for details. Args: cmdline (bool, default=True): if True, behavior will be modified based on ``sys.argv``. Note this will activate the scriptconfig ``--help``, ``--dump`` and ``--config`` interactions. Kwargs: **kw: the overrides the default config for :class:`ClfConfig`. Note, command line flags have precedence if cmdline=True. Returns: ClfHarn: a fully-defined, but uninitialized custom :class:`FitHarn` object. Example: >>> # xdoctest: +SKIP >>> kw = {'datasets': 'special:shapes256'} >>> cmdline = False >>> harn = setup_harn(cmdline, **kw) >>> harn.initialize() """ import ndsampler config = ClfConfig(default=kw) config.load(cmdline=cmdline) print('config = {}'.format(ub.repr2(config.asdict()))) nh.configure_hacks(config) coco_datasets = nh.api.Datasets.coerce(config) print('coco_datasets = {}'.format(ub.repr2(coco_datasets, nl=1))) for tag, dset in coco_datasets.items(): dset._build_hashid(hash_pixels=False) workdir = ub.ensuredir(ub.expandpath(config['workdir'])) samplers = { tag: ndsampler.CocoSampler(dset, workdir=workdir, backend=config['sampler_backend']) for tag, dset in coco_datasets.items() } for tag, sampler in ub.ProgIter(list(samplers.items()), desc='prepare frames'): sampler.frames.prepare(workers=config['workers']) torch_datasets = { 'train': ClfDataset( samplers['train'], input_dims=config['input_dims'], augmenter=config['augmenter'], ), 'vali': ClfDataset(samplers['vali'], input_dims=config['input_dims'], augmenter=False), } if config['normalize_inputs']: # Get stats on the dataset (todo: turn off augmentation for this) _dset = torch_datasets['train'] stats_idxs = kwarray.shuffle(np.arange(len(_dset)), rng=0)[0:min(1000, len(_dset))] stats_subset = torch.utils.data.Subset(_dset, stats_idxs) cacher = ub.Cacher('dset_mean', cfgstr=_dset.input_id + 'v3') input_stats = cacher.tryload() channels = ChannelSpec.coerce(config['channels']) if input_stats is None: # Use parallel workers to load data faster from netharn.data.data_containers import container_collate from functools import partial collate_fn = partial(container_collate, num_devices=1) loader = torch.utils.data.DataLoader( stats_subset, collate_fn=collate_fn, num_workers=config['workers'], shuffle=True, batch_size=config['batch_size']) # Track moving average of each fused channel stream channel_stats = { key: nh.util.RunningStats() for key in channels.keys() } assert len(channel_stats) == 1, ( 'only support one fused stream for now') for batch in ub.ProgIter(loader, desc='estimate mean/std'): for key, val in batch['inputs'].items(): try: for part in val.numpy(): channel_stats[key].update(part) except ValueError: # final batch broadcast error pass perchan_input_stats = {} for key, running in channel_stats.items(): running = ub.peek(channel_stats.values()) perchan_stats = running.simple(axis=(1, 2)) perchan_input_stats[key] = { 'std': perchan_stats['mean'].round(3), 'mean': perchan_stats['std'].round(3), } input_stats = ub.peek(perchan_input_stats.values()) cacher.save(input_stats) else: input_stats = {} torch_loaders = { tag: dset.make_loader( batch_size=config['batch_size'], num_batches=config['num_batches'], num_workers=config['workers'], shuffle=(tag == 'train'), balance=(config['balance'] if tag == 'train' else None), pin_memory=True) for tag, dset in torch_datasets.items() } initializer_ = None classes = torch_datasets['train'].classes modelkw = { 'arch': config['arch'], 'input_stats': input_stats, 'classes': classes.__json__(), 'channels': channels, } model = ClfModel(**modelkw) model._initkw = modelkw if initializer_ is None: initializer_ = nh.Initializer.coerce(config) hyper = nh.HyperParams(name=config['name'], workdir=config['workdir'], xpu=nh.XPU.coerce(config['xpu']), datasets=torch_datasets, loaders=torch_loaders, model=model, criterion=None, optimizer=nh.Optimizer.coerce(config), dynamics=nh.Dynamics.coerce(config), scheduler=nh.Scheduler.coerce(config), initializer=initializer_, monitor=(nh.Monitor, { 'minimize': ['loss'], 'patience': config['patience'], 'max_epoch': config['max_epoch'], 'smoothing': 0.0, }), other={ 'name': config['name'], 'batch_size': config['batch_size'], 'balance': config['balance'], }, extra={ 'argv': sys.argv, 'config': ub.repr2(config.asdict()), }) harn = ClfHarn(hyper=hyper) harn.preferences.update({ 'num_keep': 3, 'keep_freq': 10, 'tensorboard_groups': ['loss'], 'eager_dump_tensorboard': True, }) harn.intervals.update({}) harn.script_config = config return harn
def perterb_coco(coco_dset, **kwargs): """ Perterbs a coco dataset Args: rng (int, default=0): box_noise (int, default=0): cls_noise (int, default=0): null_pred (bool, default=False): with_probs (bool, default=False): score_noise (float, default=0.2): hacked (int, default=1): Example: >>> from kwcoco.demo.perterb import * # NOQA >>> from kwcoco.demo.perterb import _demo_construct_probs >>> import kwcoco >>> coco_dset = true_dset = kwcoco.CocoDataset.demo('shapes8') >>> kwargs = { >>> 'box_noise': 0.5, >>> 'n_fp': 3, >>> 'with_probs': 1, >>> } >>> pred_dset = perterb_coco(true_dset, **kwargs) >>> pred_dset._check_json_serializable() Ignore: import xdev from kwcoco.demo.perterb import perterb_coco # NOQA defaultkw = xdev.get_func_kwargs(perterb_coco) for k, v in defaultkw.items(): desc = '' print('{} ({}, default={}): {}'.format(k, type(v).__name__, v, desc)) """ import kwimage import kwarray # Parse kwargs rng = kwarray.ensure_rng(kwargs.get('rng', 0)) box_noise = kwargs.get('box_noise', 0) cls_noise = kwargs.get('cls_noise', 0) null_pred = kwargs.get('null_pred', False) with_probs = kwargs.get('with_probs', False) # specify an amount of overlap between true and false scores score_noise = kwargs.get('score_noise', 0.2) # Build random variables from kwarray import distributions DiscreteUniform = distributions.DiscreteUniform.seeded(rng=rng) def _parse_arg(key, default): value = kwargs.get(key, default) try: low, high = value return (low, high + 1) except Exception: return (value, value + 1) n_fp_RV = DiscreteUniform(*_parse_arg('n_fp', 0)) n_fn_RV = DiscreteUniform(*_parse_arg('n_fn', 0)) box_noise_RV = distributions.Normal(0, box_noise, rng=rng) cls_noise_RV = distributions.Bernoulli(cls_noise, rng=rng) # the values of true and false scores starts off with no overlap and # the overlap increases as the score noise increases. def _interp(v1, v2, alpha): return v1 * alpha + (1 - alpha) * v2 mid = 0.5 # true_high = 2.0 true_high = 1.0 false_low = 0.0 true_low = _interp(0, mid, score_noise) false_high = _interp(true_high, mid - 1e-3, score_noise) true_mean = _interp(0.5, .8, score_noise) false_mean = _interp(0.5, .2, score_noise) true_score_RV = distributions.TruncNormal(mean=true_mean, std=.5, low=true_low, high=true_high, rng=rng) false_score_RV = distributions.TruncNormal(mean=false_mean, std=.5, low=false_low, high=false_high, rng=rng) # Create the category hierarcy classes = coco_dset.object_categories() cids = coco_dset.cats.keys() cidxs = [classes.id_to_idx[c] for c in cids] frgnd_cx_RV = distributions.CategoryUniform(cidxs, rng=rng) new_dset = coco_dset.copy() remove_aids = [] false_anns = [] index_invalidated = False for gid in coco_dset.imgs.keys(): # Sample random variables n_fp_ = n_fp_RV() n_fn_ = n_fn_RV() true_annots = coco_dset.annots(gid=gid) aids = true_annots.aids for aid in aids: # Perterb box coordinates ann = new_dset.anns[aid] new_bbox = (np.array(ann['bbox']) + box_noise_RV(4)).tolist() new_x, new_y, new_w, new_h = new_bbox allow_neg_boxes = 0 if not allow_neg_boxes: new_w = max(new_w, 0) new_h = max(new_h, 0) ann['bbox'] = [new_x, new_y, new_w, new_h] ann['score'] = float(true_score_RV(1)[0]) if cls_noise_RV(): # Perterb class predictions ann['category_id'] = classes.idx_to_id[frgnd_cx_RV()] index_invalidated = True # Drop true positive boxes if n_fn_: import kwarray drop_idxs = kwarray.shuffle(np.arange(len(aids)), rng=rng)[0:n_fn_] remove_aids.extend(list(ub.take(aids, drop_idxs))) # Add false positive boxes if n_fp_: try: img = coco_dset.imgs[gid] scale = (img['width'], img['height']) except KeyError: scale = 100 false_boxes = kwimage.Boxes.random(num=n_fp_, scale=scale, rng=rng, format='cxywh') false_cxs = frgnd_cx_RV(n_fp_) false_scores = false_score_RV(n_fp_) false_dets = kwimage.Detections( boxes=false_boxes, class_idxs=false_cxs, scores=false_scores, classes=classes, ) for ann in list(false_dets.to_coco('new')): ann['category_id'] = classes.node_to_id[ann.pop( 'category_name')] ann['image_id'] = gid false_anns.append(ann) if null_pred: raise NotImplementedError if index_invalidated: new_dset.index.clear() new_dset._build_index() new_dset.remove_annotations(remove_aids) for ann in false_anns: new_dset.add_annotation(**ann) # Hack in the probs if with_probs: annots = new_dset.annots() pred_cids = annots.lookup('category_id') pred_cxs = np.array([classes.id_to_idx[cid] for cid in pred_cids]) pred_scores = np.array(annots.lookup('score')) # Transform the scores for the assigned class into a predicted # probability for each class. (Currently a bit hacky). pred_probs = _demo_construct_probs(pred_cxs, pred_scores, classes, rng, hacked=kwargs.get('hacked', 1)) for aid, prob in zip(annots.aids, pred_probs): new_dset.anns[aid]['prob'] = prob.tolist() return new_dset