コード例 #1
0
ファイル: main.py プロジェクト: mpelchat04/superpoint_graph
def main():
    args = parse_args()
    print('Will save to ' + args.odir)
    if not os.path.exists(args.odir):
        os.makedirs(args.odir)
    with open(os.path.join(args.odir, 'cmdline.txt'), 'w') as f:
        f.write(" ".join([
            "'" + a + "'" if (len(a) == 0 or a[0] != '-') else a
            for a in sys.argv
        ]))

    set_seed(args.seed, args.cuda)
    logging.getLogger().setLevel(
        logging.INFO)  # set to logging.DEBUG to allow for more prints
    if (args.dataset == 'sema3d' and args.db_test_name.startswith('test')) or (
            args.dataset.startswith('s3dis_02') and args.cvfold == 2):
        # needed in pytorch 0.2 for super-large graphs with batchnorm in fnet  (https://github.com/pytorch/pytorch/pull/2919)
        torch.backends.cudnn.enabled = False

    if args.use_pyg:
        torch.backends.cudnn.enabled = False

    # Decide on the dataset
    if args.dataset == 'sema3d':
        import sema3d_dataset
        dbinfo = sema3d_dataset.get_info(args)
        create_dataset = sema3d_dataset.get_datasets
    elif args.dataset == 's3dis':
        import s3dis_dataset
        dbinfo = s3dis_dataset.get_info(args)
        create_dataset = s3dis_dataset.get_datasets
    elif args.dataset == 'vkitti':
        import vkitti_dataset
        dbinfo = vkitti_dataset.get_info(args)
        create_dataset = vkitti_dataset.get_datasets
    elif args.dataset == 'custom_dataset':
        import custom_dataset  # <- to write!
        dbinfo = custom_dataset.get_info(args)
        create_dataset = custom_dataset.get_datasets
    else:
        raise NotImplementedError('Unknown dataset ' + args.dataset)

    # Create model and optimizer
    if args.resume != '':
        if args.resume == 'RESUME':
            args.resume = args.odir + '/model.pth.tar'
        model, optimizer, stats = resume(args, dbinfo)
    else:
        model = create_model(args, dbinfo)
        optimizer = create_optimizer(args, model)
        stats = []

    train_dataset, test_dataset, valid_dataset, scaler = create_dataset(args)

    print(
        f"Train dataset: {len(train_dataset)} elements - Test dataset: {len(test_dataset)} elements - "
        f"Validation dataset: {len(valid_dataset)} elements")
    ptnCloudEmbedder = pointnet.CloudEmbedder(args)
    scheduler = MultiStepLR(optimizer,
                            milestones=args.lr_steps,
                            gamma=args.lr_decay,
                            last_epoch=args.start_epoch - 1)

    def train():
        """ Trains for one epoch """
        model.train()

        loader = torch.utils.data.DataLoader(train_dataset,
                                             batch_size=args.batch_size,
                                             collate_fn=spg.eccpc_collate,
                                             num_workers=args.nworkers,
                                             shuffle=True,
                                             drop_last=True)
        if logging.getLogger().getEffectiveLevel() > logging.DEBUG:
            loader = tqdm(loader, ncols=65)

        loss_meter = tnt.meter.AverageValueMeter()
        acc_meter = tnt.meter.ClassErrorMeter(accuracy=True)
        confusion_matrix = metrics.ConfusionMatrix(dbinfo['classes'])
        t0 = time.time()

        # iterate over dataset in batches
        for bidx, (targets, GIs, clouds_data) in enumerate(loader):
            t_loader = 1000 * (time.time() - t0)

            model.ecc.set_info(GIs, args.cuda)
            label_mode_cpu, label_vec_cpu, segm_size_cpu = targets[:,
                                                                   0], targets[:,
                                                                               2:], targets[:, 1:].sum(
                                                                                   1
                                                                               )
            if args.cuda:
                label_mode, label_vec, segm_size = label_mode_cpu.cuda(
                ), label_vec_cpu.float().cuda(), segm_size_cpu.float().cuda()
            else:
                label_mode, label_vec, segm_size = label_mode_cpu, label_vec_cpu.float(
                ), segm_size_cpu.float()

            optimizer.zero_grad()
            t0 = time.time()

            embeddings = ptnCloudEmbedder.run(model, *clouds_data)
            outputs = model.ecc(embeddings)

            loss = nn.functional.cross_entropy(outputs,
                                               Variable(label_mode),
                                               weight=dbinfo["class_weights"])

            loss.backward()
            ptnCloudEmbedder.bw_hook()

            if args.grad_clip > 0:
                for p in model.parameters():
                    p.grad.data.clamp_(-args.grad_clip, args.grad_clip)
            optimizer.step()

            t_trainer = 1000 * (time.time() - t0)
            loss_meter.add(loss.item())  # pytorch 0.4

            o_cpu, t_cpu, tvec_cpu = filter_valid(outputs.data.cpu().numpy(),
                                                  label_mode_cpu.numpy(),
                                                  label_vec_cpu.numpy())
            acc_meter.add(o_cpu, t_cpu)
            confusion_matrix.count_predicted_batch(tvec_cpu,
                                                   np.argmax(o_cpu, 1))

            logging.debug(
                'Batch loss %f, Loader time %f ms, Trainer time %f ms.',
                loss.data.item(), t_loader, t_trainer)
            t0 = time.time()

        metrics_trn = {
            'acc': acc_meter.value()[0],
            'loss': loss_meter.value()[0],
            'oacc': confusion_matrix.get_overall_accuracy(),
            'avg_iou': confusion_matrix.get_average_intersection_union()
        }
        return metrics_trn

    def eval(is_valid=False):
        """ Evaluated model on test set """
        model.eval()

        if is_valid:  # validation
            loader = torch.utils.data.DataLoader(valid_dataset,
                                                 batch_size=1,
                                                 collate_fn=spg.eccpc_collate,
                                                 num_workers=args.nworkers)
        else:  # evaluation
            loader = torch.utils.data.DataLoader(test_dataset,
                                                 batch_size=1,
                                                 collate_fn=spg.eccpc_collate,
                                                 num_workers=args.nworkers)

        if logging.getLogger().getEffectiveLevel() > logging.DEBUG:
            loader = tqdm(loader, ncols=65)

        acc_meter = tnt.meter.ClassErrorMeter(accuracy=True)
        loss_meter = tnt.meter.AverageValueMeter()
        confusion_matrix = metrics.ConfusionMatrix(dbinfo['classes'])

        # iterate over dataset in batches
        for bidx, (targets, GIs, clouds_data) in enumerate(loader):
            model.ecc.set_info(GIs, args.cuda)
            label_mode_cpu, label_vec_cpu, segm_size_cpu = targets[:,
                                                                   0], targets[:,
                                                                               2:], targets[:, 1:].sum(
                                                                                   1
                                                                               ).float(
                                                                               )
            if args.cuda:
                label_mode, label_vec, segm_size = label_mode_cpu.cuda(
                ), label_vec_cpu.float().cuda(), segm_size_cpu.float().cuda()
            else:
                label_mode, label_vec, segm_size = label_mode_cpu, label_vec_cpu.float(
                ), segm_size_cpu.float()

            embeddings = ptnCloudEmbedder.run(model, *clouds_data)
            outputs = model.ecc(embeddings)

            loss = nn.functional.cross_entropy(outputs,
                                               Variable(label_mode),
                                               weight=dbinfo["class_weights"])
            loss_meter.add(loss.item())

            o_cpu, t_cpu, tvec_cpu = filter_valid(outputs.data.cpu().numpy(),
                                                  label_mode_cpu.numpy(),
                                                  label_vec_cpu.numpy())
            if t_cpu.size > 0:
                acc_meter.add(o_cpu, t_cpu)
                confusion_matrix.count_predicted_batch(tvec_cpu,
                                                       np.argmax(o_cpu, 1))

        metrics_eval = {
            'acc': meter_value(acc_meter),
            'loss': loss_meter.value()[0],
            'oacc': confusion_matrix.get_overall_accuracy(),
            'avg_iou': confusion_matrix.get_average_intersection_union(),
            'avg_acc': confusion_matrix.get_mean_class_accuracy()
        }
        return metrics_eval

    def eval_final():
        """ Evaluated model on test set in an extended way: computes estimates over multiple samples of point clouds and stores predictions """
        model.eval()

        acc_meter = tnt.meter.ClassErrorMeter(accuracy=True)
        confusion_matrix = metrics.ConfusionMatrix(dbinfo['classes'])
        collected, predictions = defaultdict(list), {}

        # collect predictions over multiple sampling seeds
        for ss in range(args.test_multisamp_n):
            test_dataset_ss = create_dataset(args, ss)[1]
            loader = torch.utils.data.DataLoader(test_dataset_ss,
                                                 batch_size=1,
                                                 collate_fn=spg.eccpc_collate,
                                                 num_workers=args.nworkers)
            if logging.getLogger().getEffectiveLevel() > logging.DEBUG:
                loader = tqdm(loader, ncols=65)

            # iterate over dataset in batches
            for bidx, (targets, GIs, clouds_data) in enumerate(loader):
                model.ecc.set_info(GIs, args.cuda)
                label_mode_cpu, label_vec_cpu, segm_size_cpu = targets[:,
                                                                       0], targets[:, 2:], targets[:, 1:].sum(
                                                                           1
                                                                       ).float(
                                                                       )

                embeddings = ptnCloudEmbedder.run(model, *clouds_data)
                outputs = model.ecc(embeddings)

                fname = clouds_data[0][0][:clouds_data[0][0].rfind('.')]
                collected[fname].append(
                    (outputs.data.cpu().numpy(), label_mode_cpu.numpy(),
                     label_vec_cpu.numpy()))

        # aggregate predictions (mean)
        for fname, lst in collected.items():
            o_cpu, t_cpu, tvec_cpu = list(zip(*lst))
            if args.test_multisamp_n > 1:
                o_cpu = np.mean(np.stack(o_cpu, 0), 0)
            else:
                o_cpu = o_cpu[0]
            t_cpu, tvec_cpu = t_cpu[0], tvec_cpu[0]
            predictions[fname] = np.argmax(o_cpu, 1)
            o_cpu, t_cpu, tvec_cpu = filter_valid(o_cpu, t_cpu, tvec_cpu)
            if t_cpu.size > 0:
                acc_meter.add(o_cpu, t_cpu)
                confusion_matrix.count_predicted_batch(tvec_cpu,
                                                       np.argmax(o_cpu, 1))

        per_class_iou = {}
        perclsiou = confusion_matrix.get_intersection_union_per_class()
        for c, name in dbinfo['inv_class_map'].items():
            per_class_iou[name] = perclsiou[c]

        metrics_final = {
            'acc': meter_value(acc_meter),
            'oacc': confusion_matrix.get_overall_accuracy(),
            'avg_iou': confusion_matrix.get_average_intersection_union(),
            'per_class_iou': per_class_iou,
            'predictions': predictions,
            'avg_acc': confusion_matrix.get_mean_class_accuracy(),
            'confusion_matrix': confusion_matrix.confusion_matrix
        }
        return metrics_final

    # Training loop
    try:
        best_iou = stats[-1]['best_iou']
    except:
        best_iou = 0
    TRAIN_COLOR = '\033[0m'
    VAL_COLOR = '\033[0;94m'
    TEST_COLOR = '\033[0;93m'
    BEST_COLOR = '\033[0;92m'
    epoch = args.start_epoch

    for epoch in range(args.start_epoch, args.epochs):
        print(f"Epoch {epoch}/{args.epochs} ({args.odir})")
        scheduler.step()

        metrics_trn = train()

        print(
            TRAIN_COLOR +
            f"-> Train Loss: {metrics_trn['loss']:.4f}  Train accuracy: {metrics_trn['acc']:.2f}%"
        )

        new_best_model = False
        if args.use_val_set:
            metrics_eval = eval(True)
            print(
                VAL_COLOR +
                f"-> Val Loss: {metrics_eval['loss']:.4f}  Val accuracy: {metrics_eval['acc']:.2f}%  "
                f"Val oAcc: {100*metrics_eval['oacc']:.2f}%  Val IoU: {100*metrics_eval['avg_iou']:.2f}%  "
                f"best ioU: {100*max(best_iou,metrics_eval['avg_iou']):.2f}%" +
                TRAIN_COLOR)
            if metrics_eval[
                    'avg_iou'] > best_iou:  # best score yet on the validation set
                print(BEST_COLOR + '-> New best model achieved!' + TRAIN_COLOR)
                best_iou = metrics_eval['avg_iou']
                new_best_model = True
                torch.save(
                    {
                        'epoch': epoch + 1,
                        'args': args,
                        'state_dict': model.state_dict(),
                        'optimizer': optimizer.state_dict()
                    }, os.path.join(args.odir, 'model.pth.tar'))
        elif epoch % args.save_nth_epoch == 0 or epoch == args.epochs - 1:
            torch.save(
                {
                    'epoch': epoch + 1,
                    'args': args,
                    'state_dict': model.state_dict(),
                    'optimizer': optimizer.state_dict()
                }, os.path.join(args.odir, 'model.pth.tar'))
        # test every test_nth_epochs
        # or test after each new model (but skip the first 5 for efficiency)
        if (not args.use_val_set and (epoch + 1) % args.test_nth_epoch
                == 0) or (args.use_val_set and new_best_model and epoch > 5):
            metrics_test = eval(False)
            print(
                TEST_COLOR +
                f"-> Test Loss: {metrics_test['loss']:.4f}  Test accuracy: {metrics_test['acc']:.2f}%  "
                f"Test oAcc: {100*metrics_test['oacc']:.2f}%  Test avgIoU: {100*metrics_test['avg_iou']:.2f}%"
                + TRAIN_COLOR)
        else:
            metrics_test = {
                'acc': 0,
                'loss': 0,
                'oacc': 0,
                'avg_iou': 0,
                'avg_acc': 0
            }

        stats.append({
            'epoch': epoch,
            'acc_trn': metrics_trn['acc'],
            'loss_trn': metrics_trn['loss'],
            'oacc_trn': metrics_trn['oacc'],
            'avg_iou_trn': metrics_trn['avg_iou'],
            'acc_test': metrics_test['acc'],
            'oacc_test': metrics_test['oacc'],
            'avg_iou_test': metrics_test['avg_iou'],
            'avg_acc_test': metrics_test['avg_acc'],
            'best_iou': best_iou
        })

        if math.isnan(metrics_trn['loss']):
            break

    if len(stats) > 0:
        with open(os.path.join(args.odir, 'trainlog.json'), 'w') as outfile:
            json.dump(stats, outfile, indent=4)

    if args.use_val_set:
        args.resume = args.odir + '/model.pth.tar'
        model, optimizer, stats = resume(args, dbinfo)
        torch.save(
            {
                'epoch': epoch + 1,
                'args': args,
                'state_dict': model.state_dict(),
                'optimizer': optimizer.state_dict()
            }, os.path.join(args.odir, 'model.pth.tar'))

    # Final evaluation
    if args.test_multisamp_n > 0 and 'test' in args.db_test_name:
        metrics_test = eval_final()
        print(
            f"-> Multisample {args.test_multisamp_n}: Test accuracy: {metrics_test['acc']:.2f}, \tTest oAcc: {metrics_test['oacc']:.2f}, "
            f"\tTest avgIoU: {metrics_test['avg_iou']:.2f}, \tTest mAcc: {metrics_test['avg_acc']:.2f}'"
        )
        with h5py.File(
                os.path.join(args.odir,
                             'predictions_' + args.db_test_name + '.h5'),
                'w') as hf:
            for fname, o_cpu in metrics_test['predictions'].items():
                hf.create_dataset(name=fname, data=o_cpu)  # (0-based classes)
        with open(
                os.path.join(args.odir,
                             'scores_' + args.db_test_name + '.json'),
                'w') as outfile:
            json.dump([{
                'epoch': args.start_epoch,
                'acc_test': metrics_test['acc'],
                'oacc_test': metrics_test['oacc'],
                'avg_iou_test': metrics_test['avg_iou'],
                'per_class_iou_test': metrics_test['per_class_iou'],
                'avg_acc_test': metrics_test['avg_acc']
            }], outfile)
        np.save(os.path.join(args.odir, 'pointwise_cm.npy'),
                metrics_test['confusion_matrix'])
コード例 #2
0
def main():
    parser = argparse.ArgumentParser(
        description=
        'Large-scale Point Cloud Semantic Segmentation with Superpoint Graphs')

    # Optimization arguments
    parser.add_argument('--wd', default=0, type=float, help='Weight decay')
    parser.add_argument('--lr',
                        default=1e-2,
                        type=float,
                        help='Initial learning rate')
    parser.add_argument(
        '--lr_decay',
        default=0.7,
        type=float,
        help='Multiplicative factor used on learning rate at `lr_steps`')
    parser.add_argument(
        '--lr_steps',
        default='[]',
        help='List of epochs where the learning rate is decreased by `lr_decay`'
    )
    parser.add_argument('--momentum', default=0.9, type=float, help='Momentum')
    parser.add_argument(
        '--epochs',
        default=10,
        type=int,
        help='Number of epochs to train. If <=0, only testing will be done.')
    parser.add_argument('--batch_size', default=2, type=int, help='Batch size')
    parser.add_argument('--optim', default='adam', help='Optimizer: sgd|adam')
    parser.add_argument(
        '--grad_clip',
        default=1,
        type=float,
        help='Element-wise clipping of gradient. If 0, does not clip')

    # Learning process arguments
    parser.add_argument('--cuda', default=1, type=int, help='Bool, use cuda')
    parser.add_argument(
        '--nworkers',
        default=0,
        type=int,
        help=
        'Num subprocesses to use for data loading. 0 means that the data will be loaded in the main process'
    )
    parser.add_argument('--test_nth_epoch',
                        default=1,
                        type=int,
                        help='Test each n-th epoch during training')
    parser.add_argument('--save_nth_epoch',
                        default=1,
                        type=int,
                        help='Save model each n-th epoch during training')
    parser.add_argument(
        '--test_multisamp_n',
        default=10,
        type=int,
        help='Average logits obtained over runs with different seeds')

    # Dataset
    parser.add_argument('--dataset',
                        default='sema3d',
                        help='Dataset name: sema3d|s3dis')
    parser.add_argument(
        '--cvfold',
        default=0,
        type=int,
        help='Fold left-out for testing in leave-one-out setting (S3DIS)')
    parser.add_argument('--odir',
                        default='results',
                        help='Directory to store results')
    parser.add_argument('--resume',
                        default='',
                        help='Loads a previously saved model.')
    parser.add_argument('--db_train_name', default='train')
    parser.add_argument('--db_test_name', default='val')
    parser.add_argument('--SEMA3D_PATH', default='datasets/semantic3d')
    parser.add_argument('--S3DIS_PATH', default='datasets/s3dis')
    parser.add_argument('--CUSTOM_SET_PATH', default='datasets/custom_set')

    # Model
    parser.add_argument(
        '--model_config',
        default='gru_10,f_8',
        help=
        'Defines the model as a sequence of layers, see graphnet.py for definitions of respective layers and acceptable arguments. In short: rectype_repeats_mv_layernorm_ingate_concat, with rectype the type of recurrent unit [gru/crf/lstm], repeats the number of message passing iterations, mv (default True) the use of matrix-vector (mv) instead vector-vector (vv) edge filters, layernorm (default True) the use of layernorms in the recurrent units, ingate (default True) the use of input gating, concat (default True) the use of state concatenation'
    )
    parser.add_argument('--seed',
                        default=1,
                        type=int,
                        help='Seed for random initialisation')
    parser.add_argument(
        '--edge_attribs',
        default=
        'delta_avg,delta_std,nlength/ld,surface/ld,volume/ld,size/ld,xyz/d',
        help=
        'Edge attribute definition, see spg_edge_features() in spg.py for definitions.'
    )

    # Point cloud processing
    parser.add_argument(
        '--pc_attribs',
        default='',
        help='Point attributes fed to PointNets, if empty then all possible.')
    parser.add_argument(
        '--pc_augm_scale',
        default=0,
        type=float,
        help=
        'Training augmentation: Uniformly random scaling in [1/scale, scale]')
    parser.add_argument(
        '--pc_augm_rot',
        default=1,
        type=int,
        help='Training augmentation: Bool, random rotation around z-axis')
    parser.add_argument(
        '--pc_augm_mirror_prob',
        default=0,
        type=float,
        help='Training augmentation: Probability of mirroring about x or y axes'
    )
    parser.add_argument(
        '--pc_augm_jitter',
        default=1,
        type=int,
        help='Training augmentation: Bool, Gaussian jittering of all attributes'
    )
    parser.add_argument(
        '--pc_xyznormalize',
        default=1,
        type=int,
        help='Bool, normalize xyz into unit ball, i.e. in [-0.5,0.5]')

    # Filter generating network
    parser.add_argument(
        '--fnet_widths',
        default='[32,128,64]',
        help=
        'List of width of hidden filter gen net layers (excluding the input and output ones, they are automatic)'
    )
    parser.add_argument(
        '--fnet_llbias',
        default=0,
        type=int,
        help='Bool, use bias in the last layer in filter gen net')
    parser.add_argument(
        '--fnet_orthoinit',
        default=1,
        type=int,
        help='Bool, use orthogonal weight initialization for filter gen net.')
    parser.add_argument(
        '--fnet_bnidx',
        default=2,
        type=int,
        help='Layer index to insert batchnorm to. -1=do not insert.')
    parser.add_argument(
        '--edge_mem_limit',
        default=30000,
        type=int,
        help=
        'Number of edges to process in parallel during computation, a low number can reduce memory peaks.'
    )

    # Superpoint graph
    parser.add_argument(
        '--spg_attribs01',
        default=1,
        type=int,
        help='Bool, normalize edge features to 0 mean 1 deviation')
    parser.add_argument('--spg_augm_nneigh',
                        default=100,
                        type=int,
                        help='Number of neighborhoods to sample in SPG')
    parser.add_argument('--spg_augm_order',
                        default=3,
                        type=int,
                        help='Order of neighborhoods to sample in SPG')
    parser.add_argument(
        '--spg_augm_hardcutoff',
        default=512,
        type=int,
        help=
        'Maximum number of superpoints larger than args.ptn_minpts to sample in SPG'
    )
    parser.add_argument(
        '--spg_superedge_cutoff',
        default=-1,
        type=float,
        help=
        'Artificially constrained maximum length of superedge, -1=do not constrain'
    )

    # Point net
    parser.add_argument(
        '--ptn_minpts',
        default=40,
        type=int,
        help=
        'Minimum number of points in a superpoint for computing its embedding.'
    )
    parser.add_argument('--ptn_npts',
                        default=128,
                        type=int,
                        help='Number of input points for PointNet.')
    parser.add_argument('--ptn_widths',
                        default='[[64,64,128,128,256], [256,64,32]]',
                        help='PointNet widths')
    parser.add_argument('--ptn_widths_stn',
                        default='[[64,64,128], [128,64]]',
                        help='PointNet\'s Transformer widths')
    parser.add_argument(
        '--ptn_nfeat_stn',
        default=11,
        type=int,
        help='PointNet\'s Transformer number of input features')
    parser.add_argument('--ptn_prelast_do', default=0, type=float)
    parser.add_argument(
        '--ptn_mem_monger',
        default=1,
        type=int,
        help=
        'Bool, save GPU memory by recomputing PointNets in back propagation.')

    args = parser.parse_args()
    args.start_epoch = 0
    args.lr_steps = ast.literal_eval(args.lr_steps)
    args.fnet_widths = ast.literal_eval(args.fnet_widths)
    args.ptn_widths = ast.literal_eval(args.ptn_widths)
    args.ptn_widths_stn = ast.literal_eval(args.ptn_widths_stn)

    print('Will save to ' + args.odir)
    if not os.path.exists(args.odir):
        os.makedirs(args.odir)
    with open(os.path.join(args.odir, 'cmdline.txt'), 'w') as f:
        f.write(" ".join([
            "'" + a + "'" if (len(a) == 0 or a[0] != '-') else a
            for a in sys.argv
        ]))

    set_seed(args.seed, args.cuda)
    logging.getLogger().setLevel(
        logging.INFO)  #set to logging.DEBUG to allow for more prints
    if (args.dataset == 'sema3d' and args.db_test_name.startswith('test')) or (
            args.dataset.startswith('s3dis_02') and args.cvfold == 2):
        # needed in pytorch 0.2 for super-large graphs with batchnorm in fnet  (https://github.com/pytorch/pytorch/pull/2919)
        torch.backends.cudnn.enabled = False

    # Decide on the dataset
    if args.dataset == 'sema3d':
        import sema3d_dataset
        dbinfo = sema3d_dataset.get_info(args)
        create_dataset = sema3d_dataset.get_datasets
    elif args.dataset == 's3dis':
        import s3dis_dataset
        dbinfo = s3dis_dataset.get_info(args)
        create_dataset = s3dis_dataset.get_datasets
    elif args.dataset == 'custom_dataset':
        import custom_dataset  #<- to write!
        dbinfo = custom_dataset.get_info(args)
        create_dataset = custom_dataset.get_datasets
    else:
        raise NotImplementedError('Unknown dataset ' + args.dataset)

    # Create model and optimizer
    if args.resume != '':
        if args.resume == 'RESUME': args.resume = args.odir + '/model.pth.tar'
        model, optimizer, stats = resume(args, dbinfo)
    else:
        model = create_model(args, dbinfo)
        optimizer = create_optimizer(args, model)
        stats = []

    train_dataset, test_dataset = create_dataset(args)
    ptnCloudEmbedder = pointnet.CloudEmbedder(args)
    scheduler = MultiStepLR(optimizer,
                            milestones=args.lr_steps,
                            gamma=args.lr_decay,
                            last_epoch=args.start_epoch - 1)

    ############
    def train():
        """ Trains for one epoch """
        model.train()

        loader = torch.utils.data.DataLoader(train_dataset,
                                             batch_size=args.batch_size,
                                             collate_fn=spg.eccpc_collate,
                                             num_workers=args.nworkers,
                                             shuffle=True,
                                             drop_last=True)
        if logging.getLogger().getEffectiveLevel() > logging.DEBUG:
            loader = tqdm(loader, ncols=100)

        loss_meter = tnt.meter.AverageValueMeter()
        acc_meter = tnt.meter.ClassErrorMeter(accuracy=True)
        confusion_matrix = metrics.ConfusionMatrix(dbinfo['classes'])
        t0 = time.time()

        # iterate over dataset in batches
        for bidx, (targets, GIs, clouds_data) in enumerate(loader):
            t_loader = 1000 * (time.time() - t0)

            model.ecc.set_info(GIs, args.cuda)
            label_mode_cpu, label_vec_cpu, segm_size_cpu = targets[:,
                                                                   0], targets[:,
                                                                               2:], targets[:, 1:].sum(
                                                                                   1
                                                                               )
            if args.cuda:
                label_mode, label_vec, segm_size = label_mode_cpu.cuda(
                ), label_vec_cpu.float().cuda(), segm_size_cpu.float().cuda()

            else:
                label_mode, label_vec, segm_size = label_mode_cpu, label_vec_cpu.float(
                ), segm_size_cpu.float()

            optimizer.zero_grad()
            t0 = time.time()

            embeddings = ptnCloudEmbedder.run(model, *clouds_data)
            outputs = model.ecc(embeddings)

            loss = nn.functional.cross_entropy(outputs, Variable(label_mode))
            loss.backward()
            ptnCloudEmbedder.bw_hook()

            if args.grad_clip > 0:
                for p in model.parameters():
                    p.grad.data.clamp_(-args.grad_clip, args.grad_clip)
            optimizer.step()

            t_trainer = 1000 * (time.time() - t0)
            loss_meter.add(loss.data[0])

            o_cpu, t_cpu, tvec_cpu = filter_valid(outputs.data.cpu().numpy(),
                                                  label_mode_cpu.numpy(),
                                                  label_vec_cpu.numpy())
            acc_meter.add(o_cpu, t_cpu)
            confusion_matrix.count_predicted_batch(tvec_cpu,
                                                   np.argmax(o_cpu, 1))

            logging.debug(
                'Batch loss %f, Loader time %f ms, Trainer time %f ms.',
                loss.data[0], t_loader, t_trainer)
            t0 = time.time()

        return acc_meter.value()[0], loss_meter.value(
        )[0], confusion_matrix.get_overall_accuracy(
        ), confusion_matrix.get_average_intersection_union()

    ############
    def eval():
        """ Evaluated model on test set """
        model.eval()

        loader = torch.utils.data.DataLoader(test_dataset,
                                             batch_size=1,
                                             collate_fn=spg.eccpc_collate,
                                             num_workers=args.nworkers)
        if logging.getLogger().getEffectiveLevel() > logging.DEBUG:
            loader = tqdm(loader, ncols=100)

        acc_meter = tnt.meter.ClassErrorMeter(accuracy=True)
        confusion_matrix = metrics.ConfusionMatrix(dbinfo['classes'])

        # iterate over dataset in batches
        for bidx, (targets, GIs, clouds_data) in enumerate(loader):
            model.ecc.set_info(GIs, args.cuda)
            label_mode_cpu, label_vec_cpu, segm_size_cpu = targets[:,
                                                                   0], targets[:,
                                                                               2:], targets[:, 1:].sum(
                                                                                   1
                                                                               ).float(
                                                                               )

            embeddings = ptnCloudEmbedder.run(model, *clouds_data)
            outputs = model.ecc(embeddings)

            o_cpu, t_cpu, tvec_cpu = filter_valid(outputs.data.cpu().numpy(),
                                                  label_mode_cpu.numpy(),
                                                  label_vec_cpu.numpy())
            if t_cpu.size > 0:
                acc_meter.add(o_cpu, t_cpu)
                confusion_matrix.count_predicted_batch(tvec_cpu,
                                                       np.argmax(o_cpu, 1))

        return meter_value(acc_meter), confusion_matrix.get_overall_accuracy(
        ), confusion_matrix.get_average_intersection_union(
        ), confusion_matrix.get_mean_class_accuracy()

    ############
    def eval_final():
        """ Evaluated model on test set in an extended way: computes estimates over multiple samples of point clouds and stores predictions """
        model.eval()

        acc_meter = tnt.meter.ClassErrorMeter(accuracy=True)
        confusion_matrix = metrics.ConfusionMatrix(dbinfo['classes'])
        collected, predictions = defaultdict(list), {}

        # collect predictions over multiple sampling seeds
        for ss in range(args.test_multisamp_n):
            test_dataset_ss = create_dataset(args, ss)[1]
            loader = torch.utils.data.DataLoader(test_dataset_ss,
                                                 batch_size=1,
                                                 collate_fn=spg.eccpc_collate,
                                                 num_workers=args.nworkers)
            if logging.getLogger().getEffectiveLevel() > logging.DEBUG:
                loader = tqdm(loader, ncols=100)

            # iterate over dataset in batches
            for bidx, (targets, GIs, clouds_data) in enumerate(loader):
                model.ecc.set_info(GIs, args.cuda)
                label_mode_cpu, label_vec_cpu, segm_size_cpu = targets[:,
                                                                       0], targets[:, 2:], targets[:, 1:].sum(
                                                                           1
                                                                       ).float(
                                                                       )

                embeddings = ptnCloudEmbedder.run(model, *clouds_data)
                outputs = model.ecc(embeddings)

                fname = clouds_data[0][0][:clouds_data[0][0].rfind('.')]
                collected[fname].append(
                    (outputs.data.cpu().numpy(), label_mode_cpu.numpy(),
                     label_vec_cpu.numpy()))

        # aggregate predictions (mean)
        for fname, lst in collected.items():
            o_cpu, t_cpu, tvec_cpu = list(zip(*lst))
            if args.test_multisamp_n > 1:
                o_cpu = np.mean(np.stack(o_cpu, 0), 0)
            else:
                o_cpu = o_cpu[0]
            t_cpu, tvec_cpu = t_cpu[0], tvec_cpu[0]
            predictions[fname] = np.argmax(o_cpu, 1)
            o_cpu, t_cpu, tvec_cpu = filter_valid(o_cpu, t_cpu, tvec_cpu)
            if t_cpu.size > 0:
                acc_meter.add(o_cpu, t_cpu)
                confusion_matrix.count_predicted_batch(tvec_cpu,
                                                       np.argmax(o_cpu, 1))

        per_class_iou = {}
        perclsiou = confusion_matrix.get_intersection_union_per_class()
        for c, name in dbinfo['inv_class_map'].items():
            per_class_iou[name] = perclsiou[c]

        return meter_value(acc_meter), confusion_matrix.get_overall_accuracy(
        ), confusion_matrix.get_average_intersection_union(
        ), per_class_iou, predictions, confusion_matrix.get_mean_class_accuracy(
        ), confusion_matrix.confusion_matrix

    ############
    # Training loop
    for epoch in range(args.start_epoch, args.epochs):
        print('Epoch {}/{} ({}):'.format(epoch, args.epochs, args.odir))
        scheduler.step()

        acc, loss, oacc, avg_iou = train()

        if (epoch + 1) % args.test_nth_epoch == 0 or epoch + 1 == args.epochs:
            acc_test, oacc_test, avg_iou_test, avg_acc_test = eval()
            print(
                '-> Train accuracy: {}, \tLoss: {}, \tTest accuracy: {}, \tTest oAcc: {}, \tTest avgIoU: {}'
                .format(acc, loss, acc_test, oacc_test, avg_iou_test))
        else:
            acc_test, oacc_test, avg_iou_test, avg_acc_test = 0, 0, 0, 0
            print('-> Train accuracy: {}, \tLoss: {}'.format(acc, loss))

        stats.append({
            'epoch': epoch,
            'acc': acc,
            'loss': loss,
            'oacc': oacc,
            'avg_iou': avg_iou,
            'acc_test': acc_test,
            'oacc_test': oacc_test,
            'avg_iou_test': avg_iou_test,
            'avg_acc_test': avg_acc_test
        })

        if epoch % args.save_nth_epoch == 0 or epoch == args.epochs - 1:
            with open(os.path.join(args.odir, 'trainlog.txt'), 'w') as outfile:
                json.dump(stats, outfile)
            torch.save(
                {
                    'epoch': epoch + 1,
                    'args': args,
                    'state_dict': model.state_dict(),
                    'optimizer': optimizer.state_dict()
                }, os.path.join(args.odir, 'model.pth.tar'))

        if math.isnan(loss): break

    if len(stats) > 0:
        with open(os.path.join(args.odir, 'trainlog.txt'), 'w') as outfile:
            json.dump(stats, outfile)

    # Final evaluation
    if args.test_multisamp_n > 0:
        acc_test, oacc_test, avg_iou_test, per_class_iou_test, predictions_test, avg_acc_test, confusion_matrix = eval_final(
        )
        print(
            '-> Multisample {}: Test accuracy: {}, \tTest oAcc: {}, \tTest avgIoU: {}, \tTest mAcc: {}'
            .format(args.test_multisamp_n, acc_test, oacc_test, avg_iou_test,
                    avg_acc_test))
        with h5py.File(
                os.path.join(args.odir,
                             'predictions_' + args.db_test_name + '.h5'),
                'w') as hf:
            for fname, o_cpu in predictions_test.items():
                hf.create_dataset(name=fname, data=o_cpu)  #(0-based classes)
        with open(
                os.path.join(args.odir,
                             'scores_' + args.db_test_name + '.txt'),
                'w') as outfile:
            json.dump([{
                'epoch': args.start_epoch,
                'acc_test': acc_test,
                'oacc_test': oacc_test,
                'avg_iou_test': avg_iou_test,
                'per_class_iou_test': per_class_iou_test,
                'avg_acc_test': avg_acc_test
            }], outfile)
        np.save(os.path.join(args.odir, 'pointwise_cm.npy'), confusion_matrix)
コード例 #3
0
ファイル: main.py プロジェクト: MMCheng/SSPC-Net
def main():
    parser = argparse.ArgumentParser(
        description=
        'Large-scale Point Cloud Semantic Segmentation with Superpoint Graphs')

    # Optimization arguments
    parser.add_argument('--wd', default=0, type=float, help='Weight decay')
    parser.add_argument('--lr',
                        default=1e-2,
                        type=float,
                        help='Initial learning rate')
    parser.add_argument(
        '--lr_decay',
        default=0.7,
        type=float,
        help='Multiplicative factor used on learning rate at `lr_steps`')
    parser.add_argument(
        '--lr_steps',
        default='[]',
        help='List of epochs where the learning rate is decreased by `lr_decay`'
    )
    parser.add_argument('--momentum', default=0.9, type=float, help='Momentum')
    parser.add_argument(
        '--epochs',
        default=400,
        type=int,
        help='Number of epochs to train. If <=0, only testing will be done.')
    parser.add_argument('--batch_size', default=2, type=int, help='Batch size')
    parser.add_argument('--optim', default='adam', help='Optimizer: sgd|adam')
    parser.add_argument(
        '--grad_clip',
        default=1,
        type=float,
        help='Element-wise clipping of gradient. If 0, does not clip')

    # Learning process arguments
    parser.add_argument('--cuda', default=1, type=int, help='Bool, use cuda')
    parser.add_argument(
        '--nworkers',
        default=0,
        type=int,
        help=
        'Num subprocesses to use for data loading. 0 means that the data will be loaded in the main process'
    )
    parser.add_argument('--test_nth_epoch',
                        default=10,
                        type=int,
                        help='Test each n-th epoch during training')
    parser.add_argument('--save_nth_epoch',
                        default=10,
                        type=int,
                        help='Save model each n-th epoch during training')
    parser.add_argument(
        '--test_multisamp_n',
        default=10,
        type=int,
        help='Average logits obtained over runs with different seeds')

    # Dataset
    parser.add_argument('--dataset',
                        default='s3dis',
                        help='Dataset name: sema3d|s3dis')
    parser.add_argument(
        '--cvfold',
        default=0,
        type=int,
        help='Fold left-out for testing in leave-one-out setting (S3DIS)')
    parser.add_argument('--odir',
                        default='results',
                        help='Directory to store results')
    parser.add_argument('--resume',
                        default='',
                        help='Loads a previously saved model.')
    parser.add_argument('--db_train_name', default='train')
    parser.add_argument('--db_test_name', default='val')
    parser.add_argument('--SEMA3D_PATH', default='datasets/semantic3d')
    parser.add_argument('--S3DIS_PATH', default='datasets/s3dis')
    parser.add_argument('--SCANNET_PATH', default='datasets/scannet')
    parser.add_argument('--VKITTI_PATH', default='datasets/vkitti')
    parser.add_argument('--CUSTOM_SET_PATH', default='datasets/custom_set')

    # Model
    parser.add_argument(
        '--model_config',
        default='gru_10,f_8',
        help=
        'Defines the model as a sequence of layers, see graphnet.py for definitions of respective layers and acceptable arguments. In short: rectype_repeats_mv_layernorm_ingate_concat, with rectype the type of recurrent unit [gru/crf/lstm], repeats the number of message passing iterations, mv (default True) the use of matrix-vector (mv) instead vector-vector (vv) edge filters, layernorm (default True) the use of layernorms in the recurrent units, ingate (default True) the use of input gating, concat (default True) the use of state concatenation'
    )
    parser.add_argument('--seed',
                        default=1,
                        type=int,
                        help='Seed for random initialisation')
    parser.add_argument(
        '--edge_attribs',
        default=
        'delta_avg,delta_std,nlength/ld,surface/ld,volume/ld,size/ld,xyz/d',
        help=
        'Edge attribute definition, see spg_edge_features() in spg.py for definitions.'
    )

    # Point cloud processing
    parser.add_argument(
        '--pc_attribs',
        default='',
        help='Point attributes fed to PointNets, if empty then all possible.')
    parser.add_argument(
        '--pc_augm_scale',
        default=0,
        type=float,
        help=
        'Training augmentation: Uniformly random scaling in [1/scale, scale]')
    parser.add_argument(
        '--pc_augm_rot',
        default=1,
        type=int,
        help='Training augmentation: Bool, random rotation around z-axis')
    parser.add_argument(
        '--pc_augm_mirror_prob',
        default=0,
        type=float,
        help='Training augmentation: Probability of mirroring about x or y axes'
    )
    parser.add_argument(
        '--pc_augm_jitter',
        default=1,
        type=int,
        help='Training augmentation: Bool, Gaussian jittering of all attributes'
    )
    parser.add_argument(
        '--pc_xyznormalize',
        default=1,
        type=int,
        help='Bool, normalize xyz into unit ball, i.e. in [-0.5,0.5]')

    # Filter generating network
    parser.add_argument(
        '--fnet_widths',
        default='[32,128,64]',
        help=
        'List of width of hidden filter gen net layers (excluding the input and output ones, they are automatic)'
    )
    parser.add_argument(
        '--fnet_llbias',
        default=0,
        type=int,
        help='Bool, use bias in the last layer in filter gen net')
    parser.add_argument(
        '--fnet_orthoinit',
        default=1,
        type=int,
        help='Bool, use orthogonal weight initialization for filter gen net.')
    parser.add_argument(
        '--fnet_bnidx',
        default=2,
        type=int,
        help='Layer index to insert batchnorm to. -1=do not insert.')
    parser.add_argument(
        '--edge_mem_limit',
        default=30000,
        type=int,
        help=
        'Number of edges to process in parallel during computation, a low number can reduce memory peaks.'
    )

    # Superpoint graph
    parser.add_argument(
        '--spg_attribs01',
        default=1,
        type=int,
        help='Bool, normalize edge features to 0 mean 1 deviation')
    parser.add_argument('--spg_augm_nneigh',
                        default=100,
                        type=int,
                        help='Number of neighborhoods to sample in SPG')
    parser.add_argument('--spg_augm_order',
                        default=3,
                        type=int,
                        help='Order of neighborhoods to sample in SPG')
    parser.add_argument(
        '--spg_augm_hardcutoff',
        default=512,
        type=int,
        help=
        'Maximum number of superpoints larger than args.ptn_minpts to sample in SPG'
    )
    parser.add_argument(
        '--spg_superedge_cutoff',
        default=-1,
        type=float,
        help=
        'Artificially constrained maximum length of superedge, -1=do not constrain'
    )

    # Point net
    parser.add_argument(
        '--ptn_minpts',
        default=40,
        type=int,
        help=
        'Minimum number of points in a superpoint for computing its embedding.'
    )
    parser.add_argument('--ptn_npts',
                        default=128,
                        type=int,
                        help='Number of input points for PointNet.')
    parser.add_argument('--ptn_widths',
                        default='[[64,64,128,128,256], [256,64,32]]',
                        help='PointNet widths')
    parser.add_argument('--ptn_widths_stn',
                        default='[[64,64,128], [128,64]]',
                        help='PointNet\'s Transformer widths')
    parser.add_argument(
        '--ptn_nfeat_stn',
        default=11,
        type=int,
        help='PointNet\'s Transformer number of input features')
    parser.add_argument('--ptn_prelast_do', default=0, type=float)
    parser.add_argument(
        '--ptn_mem_monger',
        default=1,
        type=int,
        help=
        'Bool, save GPU memory by recomputing PointNets in back propagation.')
    parser.add_argument('--ignore_label',
                        type=int,
                        default=255,
                        help='ignore label')

    # extension
    parser.add_argument('--extension_th', type=float, default=0.95)
    parser.add_argument('--loss_w1',
                        type=float,
                        default=1.0,
                        help='weight of cross entropy loss')
    parser.add_argument('--loss_w2',
                        type=float,
                        default=1.0,
                        help='weight of cross entropy loss of extension')
    parser.add_argument('--ext_epoch_gap',
                        type=int,
                        default=40,
                        help='interval of epoch for accumulated extension')
    parser.add_argument('--ext_drop',
                        type=float,
                        default=0.9,
                        help='dropout ratio for accumulated extension')
    parser.add_argument(
        '--single_ext_max',
        type=int,
        default=40,
        help='To reduce memory cost,maximum points for a single extension')
    parser.add_argument(
        '--max_labeled_att',
        type=int,
        default=400,
        help=
        'To reduce memory cost, maximum labeled points for extension attention'
    )
    parser.add_argument(
        '--max_ext_att_loss',
        type=int,
        default=350,
        help=
        'To reduce memory cost, maximum extension points participated in attention and loss'
    )

    parser.add_argument('--data_mode',
                        type=str,
                        default='voxel',
                        choices=['point', 'voxel'])
    parser.add_argument('--train_gpu',
                        type=int,
                        default=0,
                        help='ignore label')
    parser.add_argument(
        '--metric_ignore_class',
        type=int,
        default=None,
        help='maximum extension points participated in attention and loss')

    args = parser.parse_args()
    args.start_epoch = 0
    args.lr_steps = ast.literal_eval(args.lr_steps)
    args.fnet_widths = ast.literal_eval(args.fnet_widths)
    args.ptn_widths = ast.literal_eval(args.ptn_widths)
    args.ptn_widths_stn = ast.literal_eval(args.ptn_widths_stn)
    args.extension_dir = os.path.join(
        args.odir,
        'extension_log')  # To store the extension intermediate results

    print(args)

    print('Will save to ' + args.odir)
    if not os.path.exists(args.odir):
        os.makedirs(args.odir)
    if not os.path.exists(args.extension_dir):
        os.makedirs(args.extension_dir)
    with open(os.path.join(args.odir, 'cmdline.txt'), 'w') as f:
        f.write(" ".join([
            "'" + a + "'" if (len(a) == 0 or a[0] != '-') else a
            for a in sys.argv
        ]))

    set_seed(args.seed, args.cuda)
    if (args.dataset == 'sema3d' and args.db_test_name.startswith('test')) or (
            args.dataset.startswith('s3dis_02') and args.cvfold == 2):
        # needed in pytorch 0.2 for super-large graphs with batchnorm in fnet  (https://github.com/pytorch/pytorch/pull/2919)
        torch.backends.cudnn.enabled = False

    # Decide on the dataset
    if args.dataset == 's3dis':
        import s3dis_dataset
        dbinfo = s3dis_dataset.get_info(args)
        create_dataset = s3dis_dataset.get_datasets
        args.n_labels = 13
    elif args.dataset == 'scannet':
        import scannet_dataset
        dbinfo = scannet_dataset.get_info(args)
        create_dataset = scannet_dataset.get_datasets
        args.n_labels = 20
    elif args.dataset == 'vkitti':
        import vkitti_dataset
        dbinfo = vkitti_dataset.get_info(args)
        create_dataset = vkitti_dataset.get_datasets
        args.n_labels = 13

    elif args.dataset == 'custom_dataset':
        import custom_dataset  #<- to write!
        dbinfo = custom_dataset.get_info(args)
        create_dataset = custom_dataset.get_datasets
    else:
        raise NotImplementedError('Unknown dataset ' + args.dataset)

    # Create model and optimizer
    if args.resume != '':
        model, optimizer, stats = resume(args, dbinfo)
    else:
        model = create_model(args, dbinfo)
        optimizer = create_optimizer(args, model)
        stats = []

    train_dataset, test_dataset = create_dataset(args)
    ptnCloudEmbedder = pointnet.CloudEmbedder(args)
    scheduler = MultiStepLR(optimizer,
                            milestones=args.lr_steps,
                            gamma=args.lr_decay,
                            last_epoch=args.start_epoch - 1)

    ############
    def train(epoch):
        """ Trains for one epoch """
        args.ext_epoch = epoch
        model.train()

        loader = torch.utils.data.DataLoader(train_dataset,
                                             batch_size=args.batch_size,
                                             collate_fn=spg.eccpc_collate,
                                             num_workers=args.nworkers,
                                             shuffle=True,
                                             drop_last=True)

        loss_meter = tnt.meter.AverageValueMeter()
        loss_ext_meter = tnt.meter.AverageValueMeter()
        loss_att_meter = tnt.meter.AverageValueMeter()
        acc_meter = tnt.meter.ClassErrorMeter(accuracy=True)
        confusion_matrix = metrics.ConfusionMatrix(
            dbinfo['classes'], ignore_label=args.metric_ignore_class)
        confusion_matrix_ext = metrics.ConfusionMatrix(
            dbinfo['classes'], ignore_label=args.metric_ignore_class)
        confusion_matrix_ext_epoch = metrics.ConfusionMatrix(
            dbinfo['classes'], ignore_label=args.metric_ignore_class)
        t0 = time.time()
        epoch_time = time.time()
        batch_time = AverageMeter()
        end = time.time()

        # iterate over dataset in batches
        for bidx, (targets, GIs, clouds_data, clouds_orig, edges_for_ext,
                   fnames, ext_data, num_sp_list) in enumerate(loader):
            print('fnames: {}'.format(fnames))
            t_loader = time.time() - t0

            model.ecc.set_info(GIs, args.cuda)
            weak_label_mode_cpu, label_mode_cpu, label_vec_cpu, segm_size_cpu = targets[:,
                                                                                        0], targets[:,
                                                                                                    1], targets[:,
                                                                                                                2:], targets[:, 2:].sum(
                                                                                                                    1
                                                                                                                )
            ext_mask, extension_sub_list, extension_full_list = ext_data

            if args.cuda:
                label_mode, label_vec, segm_size, weak_label_mode = label_mode_cpu.cuda(
                ), label_vec_cpu.float().cuda(), segm_size_cpu.float().cuda(
                ), weak_label_mode_cpu.cuda()
            else:
                label_mode, label_vec, segm_size, weak_label_mode = label_mode_cpu, label_vec_cpu.float(
                ), segm_size_cpu.float(), weak_label_mode_cpu

            optimizer.zero_grad()
            t0 = time.time()

            print('num_weak_label/num_sp_all: {}/{}'.format(
                torch.sum(weak_label_mode < args.n_labels + 1),
                weak_label_mode.shape[0]))

            embeddings = ptnCloudEmbedder.run(model, *clouds_data)
            outputs, rnn_fea = model.ecc(embeddings)
            o_cpu, t_cpu, tvec_cpu = filter_valid(outputs.data.cpu().numpy(),
                                                  label_mode_cpu.numpy(),
                                                  label_vec_cpu.numpy())

            # extension
            input = clouds_orig.cuda()
            edges_for_ext = edges_for_ext.cuda()
            ext_weak_label_cuda = torch.argmax(outputs, dim=1, keepdim=False)

            weak_label_cat = weak_label_mode
            weak_label_cat[ext_mask > 0] = ext_weak_label_cuda[ext_mask > 0]

            if (epoch > 0) and (epoch % args.ext_epoch_gap == 0):
                output1, weak_label1, output2, weak_label2, extend_idx, _ = extension_accum2(
                    input,
                    outputs,
                    embeddings,
                    weak_label_cat,
                    edges_for_ext,
                    th=args.extension_th,
                    ext_max=args.single_ext_max)
                print('{}/{} ~ {:.2f} points with labels.'.format(
                    outputs.shape[0], output1.shape[0],
                    output1.shape[0] / outputs.shape[0] * 100))
                print('extension points: {}'.format(extend_idx.shape))
            else:
                extend_idx = torch.Tensor([])
                output2 = torch.Tensor([])
                weak_label2 = torch.Tensor([])

            # loss
            mask1 = weak_label_mode != args.ignore_label
            outputs_valid1 = outputs[mask1, :]
            weak_label1 = weak_label_mode[mask1]
            loss1_cro = nn.functional.cross_entropy(
                outputs_valid1, weak_label1, ignore_index=args.ignore_label)

            ###### extension dropout
            # labeled points: outputs_valid1, weak_label1
            labeled_idx = torch.nonzero(mask1).squeeze().long()  # Nsp_label
            # previous extension points:
            if torch.sum(ext_mask > 0) > 1:
                pre_ext_outputs = outputs[ext_mask > 0, :]  # Nsp_pre_ext
                pre_ext_fea = rnn_fea[ext_mask > 0, :]
                pre_ext_pseudo_label = torch.argmax(pre_ext_outputs,
                                                    dim=1,
                                                    keepdim=False)
                pre_ext_idx = torch.nonzero(ext_mask > 0).squeeze().long()
            # current extension points:
            if extend_idx.shape[0] > 1:
                cur_ext_outputs = output2
                cur_ext_pred_label = weak_label2
                cur_ext_idx = extend_idx
                cur_ext_fea = rnn_fea[extend_idx, :]

            # extension concat
            if (torch.sum(ext_mask > 0) > 1) & (extend_idx.shape[0] > 1):
                ext_outputs_cat = torch.cat((pre_ext_outputs, cur_ext_outputs),
                                            0)
                ext_label_cat = torch.cat((pre_ext_pseudo_label.unsqueeze(-1),
                                           cur_ext_pred_label.unsqueeze(-1)),
                                          0).squeeze(-1)
                ext_idx_cat = torch.cat(
                    (pre_ext_idx.unsqueeze(-1), cur_ext_idx.unsqueeze(-1)),
                    0).squeeze(-1)
                ext_fea_cat = torch.cat((pre_ext_fea, cur_ext_fea), 0)
            elif extend_idx.shape[0] > 1:  # only current extension points
                ext_outputs_cat = cur_ext_outputs
                ext_label_cat = cur_ext_pred_label
                ext_idx_cat = cur_ext_idx
                ext_fea_cat = cur_ext_fea
            elif torch.sum(ext_mask > 0) > 1:  # only previous extension points
                ext_outputs_cat = pre_ext_outputs
                ext_label_cat = pre_ext_pseudo_label
                ext_idx_cat = pre_ext_idx
                ext_fea_cat = pre_ext_fea
            else:
                ext_outputs_cat = None
                ext_label_cat = None
                ext_idx_cat = None
                ext_fea_cat = None

            # compute the cluster center and the distances and then dropout with ratio
            if (ext_idx_cat is not None) and (ext_idx_cat.shape[0] > 20):
                ext_idxs_sample = [
                ]  # sampled id of each extension points in all the extension points
                unique_classes = torch.unique(weak_label1)
                ext_idx_idx = torch.Tensor(
                    list(range(ext_idx_cat.shape[0]))
                ).cuda(
                )  # id of each extension points in all the extension points
                for i in range(unique_classes.shape[0]):
                    sp = unique_classes[i]
                    fea_label = outputs_valid1[weak_label1 ==
                                               sp]  # Nsp_label_class*13
                    fea_ext = ext_outputs_cat[ext_label_cat ==
                                              sp]  # Nsp_ext_class*13
                    ext_idxs = ext_idx_idx[ext_label_cat ==
                                           sp]  # Nsp_ext_class

                    if (fea_ext.shape[0] > 5) & (fea_label.shape[0] > 0):
                        num_retain = math.floor(fea_ext.shape[0] *
                                                args.ext_drop)

                        cluster_center = torch.sum(
                            fea_label, dim=0, keepdim=True) + 0.5 * torch.sum(
                                fea_ext, dim=0, keepdim=True)
                        cluster_center = cluster_center / (
                            fea_label.shape[0] + fea_ext.shape[0])  # 1*13

                        dis = fea_ext - cluster_center  # Nsp_ext*13
                        dis = torch.norm(dis, dim=1)  # Nsp
                        _, idxs = torch.sort(dis, dim=0, descending=False)
                        ext_idxs_sample.append(
                            ext_idxs[idxs[:num_retain]].unsqueeze(-1))
                    elif len(ext_idxs) > 0:
                        ext_idxs_sample.append(ext_idxs.unsqueeze(-1))

                ext_idxs_sample = torch.cat(ext_idxs_sample,
                                            0).squeeze(-1).long()

                ext_idxs_retain = ext_idx_cat[ext_idxs_sample]
                ext_output_retain = ext_outputs_cat[ext_idxs_sample, :]
                ext_fea_retain = ext_fea_cat[ext_idxs_sample, :]
                ext_label_retain = ext_label_cat[ext_idxs_sample]

            else:
                ext_idxs_retain = ext_idx_cat
                ext_output_retain = ext_outputs_cat
                ext_fea_retain = ext_fea_cat
                ext_label_retain = ext_label_cat

            if (ext_idxs_retain
                    is not None) and (ext_idxs_retain.shape[0] > 2):
                lab_fea = rnn_fea[
                    labeled_idx, :]  # M*352, including the previous extension points
                lab_lab = weak_label1
                if lab_fea.shape[0] > args.max_labeled_att:
                    ii = random.sample(range(lab_fea.shape[0]),
                                       k=args.max_labeled_att)
                    lab_fea = lab_fea[ii, :]
                    lab_lab = lab_lab[ii]
                    lab_idxs_sample = ii
                if ext_idxs_retain.shape[0] > args.max_ext_att_loss:
                    ii = random.sample(range(ext_idxs_retain.shape[0]),
                                       k=args.max_ext_att_loss)
                    # ext_idxs_retain_sample = ext_idxs_retain[ii]
                    ext_idxs_retain_sample = ii
                else:
                    # ext_idxs_retain_sample = ext_idxs_retain
                    ext_idxs_retain_sample = list(
                        range(ext_idxs_retain.shape[0]))
                ext_fea = rnn_fea[ext_idxs_retain_sample, :]  # N*352

                # coupled attention
                outputs_att_lab = model.att_lab(lab_fea, ext_fea)
                outputs_att_ext = model.att_ext(ext_fea, lab_fea)

                loss_att_lab_cro = nn.functional.cross_entropy(
                    outputs_att_lab, lab_lab)
                loss_att_meter.add(loss_att_lab_cro.item())

                loss_att_ext_cro = nn.functional.cross_entropy(
                    outputs_att_ext, ext_label_retain[ext_idxs_retain_sample])
                loss_ext_meter.add(loss_att_ext_cro.item())

                loss = args.loss_w1 * loss1_cro + args.loss_w2 * (
                    loss_att_lab_cro + loss_att_ext_cro)

                confusion_matrix_ext.count_predicted_batch(
                    tvec_cpu[ext_idxs_retain.data.cpu().numpy(), :],
                    np.argmax(outputs[ext_idxs_retain, :].data.cpu().numpy(),
                              1))
                print('{} point extend. acc: {:3f}, macc: {:3f}'.format(
                    ext_idxs_retain.shape[0],
                    confusion_matrix_ext.get_overall_accuracy(),
                    confusion_matrix_ext.get_mean_class_accuracy()))
                if extend_idx.shape[0] > 1:
                    confusion_matrix_ext_epoch.count_predicted_batch(
                        tvec_cpu[extend_idx.data.cpu().numpy(), :],
                        np.argmax(outputs[extend_idx, :].data.cpu().numpy(),
                                  1))
                    print(
                        '{} point extend current epoch. acc: {:3f}, macc: {:3f}'
                        .format(
                            extend_idx.shape[0],
                            confusion_matrix_ext_epoch.get_overall_accuracy(),
                            confusion_matrix_ext_epoch.get_mean_class_accuracy(
                            )))

                # update the extension
                extend_idx = ext_idxs_retain.data.cpu().numpy().astype(
                    np.int32)
                weak_label2 = ext_label_retain.data.cpu().numpy().astype(
                    np.int32)
                num_sp_array = np.cumsum(np.array(num_sp_list))

                for b in range(num_sp_array.shape[0]):
                    if b == 0:
                        sp_start = 0
                    else:
                        sp_start = num_sp_array[b - 1]
                    sp_end = num_sp_array[b]

                    mask = (extend_idx >= sp_start) & (extend_idx < sp_end)
                    if np.sum(mask) > 0:
                        extend_idx_batch = extend_idx[mask] - sp_start
                        extend_label_batch = weak_label2[mask]

                        extension_sub_batch = extension_sub_list[b].astype(
                            np.int32)  # Nsp*2
                        extension_full_batch = extension_full_list[b].astype(
                            np.int32)  # N*2

                        extension_full_batch[extension_sub_batch[
                            extend_idx_batch, 0]] = extend_label_batch
                        current_fname = fnames[b]
                        np.savetxt(os.path.join(
                            args.extension_dir, 'epoch_{:d}'.format(
                                int(epoch // args.ext_epoch_gap)),
                            '{}.txt'.format(current_fname)),
                                   extension_full_batch,
                                   fmt='%d')

            else:
                loss = loss1_cro

            loss.backward()
            ptnCloudEmbedder.bw_hook()

            if args.grad_clip > 0:
                for p in model.parameters():
                    if p.grad is not None:
                        p.grad.data.clamp_(-args.grad_clip, args.grad_clip)
            optimizer.step()

            t_trainer = time.time() - t0
            # loss_meter.add(loss.data[0]) # pytorch 0.3
            loss_meter.add(loss.item())  # pytorch 0.4

            acc_meter.add(o_cpu, t_cpu)
            confusion_matrix.count_predicted_batch(tvec_cpu,
                                                   np.argmax(o_cpu, 1))

            batch_time.update(time.time() - end)
            end = time.time()

            print(
                'Batch {}/{} - loss {:.3f}/{:.3f}, acc {:.3f}, lr {:.3f}, Loader time {:.3f}, Trainer time {:.3f}, Batch time {:.3f}/{:.3f}.'
                .format(bidx + 1, len(loader), loss.item(),
                        loss_meter.value()[0],
                        confusion_matrix.get_overall_accuracy(),
                        get_lr(optimizer), t_loader, t_trainer, batch_time.val,
                        batch_time.avg))
            t0 = time.time()

        if args.ext_epoch % args.ext_epoch_gap == (
                args.ext_epoch_gap -
                1):  # a new extension folder need to be built
            shutil.copytree(
                os.path.join(
                    args.extension_dir, 'epoch_{}'.format(
                        int(args.ext_epoch // args.ext_epoch_gap))),
                os.path.join(
                    args.extension_dir, 'epoch_{}'.format(
                        int(args.ext_epoch // args.ext_epoch_gap) + 1)))
        return acc_meter.value()[0], confusion_matrix.get_overall_accuracy(), confusion_matrix.get_mean_class_accuracy(), confusion_matrix.get_average_intersection_union(), loss_meter.value()[0], time.time()-epoch_time, \
                    confusion_matrix_ext_epoch.get_overall_accuracy(), confusion_matrix_ext_epoch.get_mean_class_accuracy(), confusion_matrix_ext_epoch.get_average_intersection_union()

    ############
    def eval():
        """ Evaluated model on test set """
        model.eval()

        loader = torch.utils.data.DataLoader(test_dataset,
                                             batch_size=1,
                                             collate_fn=spg.eccpc_collate_test,
                                             num_workers=args.nworkers,
                                             drop_last=False)

        acc_meter = tnt.meter.ClassErrorMeter(accuracy=True)
        confusion_matrix = metrics.ConfusionMatrix(
            dbinfo['classes'], ignore_label=args.metric_ignore_class)
        test_time = time.time()
        # iterate over dataset in batches
        for bidx, (targets, GIs, clouds_data, clouds_orig, edges_for_ext,
                   fnames) in enumerate(loader):
            model.ecc.set_info(GIs, args.cuda)
            # label_mode_cpu, label_vec_cpu, segm_size_cpu = targets[:,0], targets[:,2:], targets[:,1:].sum(1).float()
            weak_label_mode_cpu, label_mode_cpu, label_vec_cpu, segm_size_cpu = targets[:,
                                                                                        0], targets[:,
                                                                                                    1], targets[:,
                                                                                                                2:], targets[:, 2:].sum(
                                                                                                                    1
                                                                                                                )

            embeddings = ptnCloudEmbedder.run(model, *clouds_data)
            outputs, _ = model.ecc(embeddings)

            o_cpu, t_cpu, tvec_cpu = filter_valid(outputs.data.cpu().numpy(),
                                                  label_mode_cpu.numpy(),
                                                  label_vec_cpu.numpy())
            if t_cpu.size > 0:
                acc_meter.add(o_cpu, t_cpu)
                confusion_matrix.count_predicted_batch(tvec_cpu,
                                                       np.argmax(o_cpu, 1))
                print('{}/{}-{} outputs: {}, acc: {}, macc: {}'.format(
                    bidx, len(loader), fnames[0], outputs.shape,
                    confusion_matrix.get_overall_accuracy(),
                    confusion_matrix.get_mean_class_accuracy()))

        return meter_value(acc_meter), confusion_matrix.get_overall_accuracy(
        ), confusion_matrix.get_mean_class_accuracy(
        ), confusion_matrix.get_average_intersection_union(
        ), time.time() - test_time

    ############
    def eval_final():
        """ Evaluated model on test set in an extended way: computes estimates over multiple samples of point clouds and stores predictions """
        model.eval()

        acc_meter = tnt.meter.ClassErrorMeter(accuracy=True)
        confusion_matrix = metrics.ConfusionMatrix(
            dbinfo['classes'], ignore_label=args.metric_ignore_class)
        collected, predictions = defaultdict(list), {}

        # collect predictions over multiple sampling seeds
        for ss in range(args.test_multisamp_n):
            test_dataset_ss = create_dataset(args, ss)[1]
            loader = torch.utils.data.DataLoader(
                test_dataset_ss,
                batch_size=1,
                collate_fn=spg.eccpc_collate_test,
                num_workers=args.nworkers)

            # iterate over dataset in batches
            for bidx, (targets, GIs, clouds_data, clouds_orig, edges_for_ext,
                       _) in enumerate(loader):
                model.ecc.set_info(GIs, args.cuda)
                weak_label_mode_cpu, label_mode_cpu, label_vec_cpu, segm_size_cpu = targets[:,
                                                                                            0], targets[:,
                                                                                                        1], targets[:,
                                                                                                                    2:], targets[:, 2:].sum(
                                                                                                                        1
                                                                                                                    )

                embeddings = ptnCloudEmbedder.run(model, *clouds_data)
                outputs, _ = model.ecc(embeddings)

                fname = clouds_data[0][0][:clouds_data[0][0].rfind('.')]
                collected[fname].append(
                    (outputs.data.cpu().numpy(), label_mode_cpu.numpy(),
                     label_vec_cpu.numpy()))

        # aggregate predictions (mean)
        for fname, lst in collected.items():
            o_cpu, t_cpu, tvec_cpu = list(zip(*lst))
            if args.test_multisamp_n > 1:
                o_cpu = np.mean(np.stack(o_cpu, 0), 0)
            else:
                o_cpu = o_cpu[0]
            t_cpu, tvec_cpu = t_cpu[0], tvec_cpu[0]
            predictions[fname] = np.argmax(o_cpu, 1)
            o_cpu, t_cpu, tvec_cpu = filter_valid(o_cpu, t_cpu, tvec_cpu)
            if t_cpu.size > 0:
                acc_meter.add(o_cpu, t_cpu)
                confusion_matrix.count_predicted_batch(tvec_cpu,
                                                       np.argmax(o_cpu, 1))

        per_class_iou = {}
        perclsiou = confusion_matrix.get_intersection_union_per_class()
        for c, name in dbinfo['inv_class_map'].items():
            per_class_iou[name] = perclsiou[c]

        return meter_value(acc_meter), confusion_matrix.get_overall_accuracy(
        ), confusion_matrix.get_average_intersection_union(
        ), per_class_iou, predictions, confusion_matrix.get_mean_class_accuracy(
        ), confusion_matrix.confusion_matrix

    ############
    # Training loop
    for epoch in range(args.start_epoch, args.epochs):
        print('Epoch {}/{} ({}):'.format(epoch, args.epochs, args.odir))
        # scheduler.step()

        _, acc, macc, miou, loss, epoch_time, ext_acc, ext_macc, ext_miou = train(
            epoch)

        if (epoch + 1) % args.test_nth_epoch == 0 or epoch + 1 == args.epochs:
            _, test_acc, test_macc, test_miou, test_time = eval()
            print(
                '-> [{}/{}] Train results as epoch: mIou/mAcc/allAcc/loss/time {:.4f}/{:.4f}/{:.4f}/{:.4f}/{:.4f} '
                .format(epoch + 1, args.epochs, miou, macc, acc, loss,
                        epoch_time))
            print(
                '-> [{}/{}] Train extension results as epoch: mIou/mAcc/allAcc {:.4f}/{:.4f}/{:.4f}/ '
                .format(epoch + 1, args.epochs, ext_miou, ext_macc, ext_acc))
            print(
                '-> [{}/{}] Test results as epoch: mIou/mAcc/allAcc/time {:.4f}/{:.4f}/{:.4f}/{:.4f} '
                .format(epoch + 1, args.epochs, test_miou, test_macc, test_acc,
                        test_time))
        else:
            test_acc, test_miou, test_macc = 0, 0, 0
            print(
                '-> Train accuracy: {:.3f}, \tLoss: {:.3f}, \tEpoch_time: {:.3f}'
                .format(acc, loss, epoch_time))

        stats.append({
            'epoch': epoch,
            'loss': loss,
            'oacc': acc,
            'miou': miou,
            'oacc_test': test_acc,
            'test_miou': test_miou,
            'test_macc': test_macc
        })

        if epoch % args.save_nth_epoch == 0 or epoch == args.epochs - 1:
            with open(os.path.join(args.odir, 'trainlog.txt'), 'w') as outfile:
                json.dump(stats, outfile)
            torch.save(
                {
                    'epoch': epoch + 1,
                    'args': args,
                    'state_dict': model.state_dict(),
                    'optimizer': optimizer.state_dict()
                },
                os.path.join(args.odir, 'epoch{}_model.pth.tar'.format(epoch)))

        if math.isnan(loss): break
        scheduler.step()

    if len(stats) > 0:
        with open(os.path.join(args.odir, 'trainlog.txt'), 'w') as outfile:
            json.dump(stats, outfile)

    # Final evaluation
    if args.test_multisamp_n > 0:
        acc_test, oacc_test, avg_iou_test, per_class_iou_test, predictions_test, avg_acc_test, confusion_matrix = eval_final(
        )
        print(
            '-> Multisample {}: Test accuracy: {}, \tTest oAcc: {}, \tTest avgIoU: {}, \tTest mAcc: {}'
            .format(args.test_multisamp_n, acc_test, oacc_test, avg_iou_test,
                    avg_acc_test))
        with h5py.File(
                os.path.join(args.odir,
                             'predictions_' + args.db_test_name + '.h5'),
                'w') as hf:
            for fname, o_cpu in predictions_test.items():
                hf.create_dataset(name=fname, data=o_cpu)  #(0-based classes)
        with open(
                os.path.join(args.odir,
                             'scores_' + args.db_test_name + '.txt'),
                'w') as outfile:
            json.dump([{
                'epoch': args.start_epoch,
                'acc_test': acc_test,
                'oacc_test': oacc_test,
                'avg_iou_test': avg_iou_test,
                'per_class_iou_test': per_class_iou_test,
                'avg_acc_test': avg_acc_test
            }], outfile)
        np.save(os.path.join(args.odir, 'pointwise_cm.npy'), confusion_matrix)