//  [python]
def evaluate(**kwargs):
    best_loss = kwargs['best_loss']
    best_acc = kwargs['best_acc']
    best_auc = kwargs['best_auc']
    global_step = kwargs['global_step']

    capsule_net.eval()
    test_loss = np.zeros(3)
    targets, preds_coarse, preds_fine, preds_dist = [], [], [], []

    with torch.no_grad():
        for batch_id, (data, target, _) in enumerate(test_loader):
            data, target = data.cuda(), target.cuda()
            p_coarse, avg_activation_map = capsule_net(data, target)
            p_coarse_length = get_vector_length(p_coarse)
            loss = capsule_loss(p_coarse_length, target)
            targets += [target]
            preds_coarse += [p_coarse_length]
            test_loss[0] += loss

            ##################################
            # Distillation (Localization and Refinement)
            ##################################
            crop_mask = F.upsample_bilinear(avg_activation_map, size=(data.size(2), data.size(3))) > options.theta_c
            crop_images = []
            for batch_index in range(crop_mask.size(0)):
                nonzero_indices = torch.nonzero(crop_mask[batch_index, 0, ...])
                height_min = nonzero_indices[:, 0].min()
                height_max = nonzero_indices[:, 0].max()
                width_min = nonzero_indices[:, 1].min()
                width_max = nonzero_indices[:, 1].max()
                crop_images.append(F.upsample_bilinear(
                    data[batch_index:batch_index + 1, :, height_min:height_max, width_min:width_max],
                    size=options.img_h))
            crop_images = torch.cat(crop_images, dim=0)

            p_fine, _ = capsule_net(crop_images, target)
            p_fine_length = get_vector_length(p_fine)
            loss = capsule_loss(p_fine_length, target)
            preds_fine += [p_fine_length]
            test_loss[1] += loss

            p_dist = (p_coarse + p_fine) / 2
            p_dist_length = get_vector_length(p_dist)
            test_loss[2] += capsule_loss(p_dist_length, target)
            preds_dist += [p_dist_length]

        test_loss /= (batch_id + 1)
        # compute metrics
        metrics_coarse = compute_metrics(torch.cat(preds_coarse).cpu(), torch.cat(targets).cpu())
        metrics_fine = compute_metrics(torch.cat(preds_fine).cpu(), torch.cat(targets).cpu())
        metrics_dist = compute_metrics(torch.cat(preds_dist).cpu(), torch.cat(targets).cpu())

        # compute the confusion matrix
        y_true = torch.cat(targets).cpu().numpy()
        y_pred = torch.cat(preds_dist).cpu().numpy().argmax(axis=1)
        conf_mat = confusion_matrix(y_true, y_pred)
        print(conf_mat)
        conf_mat = conf_mat.flatten()
        conf_mat_text = ''
        for val in conf_mat:
            conf_mat_text = conf_mat_text + str(val) + ', '
        log_string(conf_mat_text)

        # check for improvement
        loss_str, acc_str, auc_str = '', '', ''
        if test_loss[2] <= best_loss:
            loss_str, best_loss = '(improved)', test_loss[2]
        if metrics_dist['acc'] >= best_acc:
            acc_str, best_acc = '(improved)', metrics_dist['acc']
        if metrics_dist['auc'] >= best_auc:
            auc_str, best_auc = '(improved)', metrics_dist['auc']

        # display
        log_string(" - (Coarse-grained)     loss: {0:.4f}, acc: {1:.02%}, auc: {2:.02%}"
                   .format(test_loss[0], metrics_coarse['acc'], metrics_coarse['auc']))
        log_string(" - (Fine-grained)       loss: {0:.4f}, acc: {1:.02%}, auc: {2:.02%}"
                   .format(test_loss[1], metrics_fine['acc'], metrics_fine['auc']))
        log_string(" - (Distilled)          loss: {0:.4f} {1}, acc: {2:.02%}{3}, auc: {4:.02%}{5}"
                   .format(test_loss[2], loss_str, metrics_dist['acc'], acc_str, metrics_dist['auc'], auc_str))
        # write to TensorBoard
        info = {'loss/coarse-grained': test_loss[0],
                'loss/fine-grained': test_loss[1],
                'loss/distilled': test_loss[2],
                'accuracy/coarse-grained': metrics_coarse['acc'],
                'accuracy/fine-grained': metrics_fine['acc'],
                'accuracy/distilled': metrics_dist['acc'],
                'AUC/coarse-grained': metrics_coarse['auc'],
                'AUC/fine-grained': metrics_fine['auc'],
                'AUC/distilled': metrics_dist['auc']}
        for tag, value in info.items():
            test_logger.scalar_summary(tag, value, global_step)

        # save checkpoint model
        state_dict = capsule_net.state_dict()
        for key in state_dict.keys():
            state_dict[key] = state_dict[key].cpu()
        save_path = os.path.join(model_dir, '{}.ckpt'.format(global_step))
        torch.save({
            'global_step': global_step,
            'acc': metrics_dist['acc'],
            'auc': metrics_dist['auc'],
            'save_dir': model_dir,
            'state_dict': state_dict},
            save_path)
        log_string('Model saved at: {}'.format(save_path))
        log_string('--' * 30)
        return best_loss, best_acc, best_auc