【Unbiased Scene Graph Generation from Biased Training】场景图评估指标详解(附代码解读)

前文

这篇文章所提到的场景图评估方法均来自Unbiased Scene Graph Generation from Biased Training。下文是我引用作者的表述,结合作者源代码的解读所给出的对于每个评估指标更加详细的表述,同时附带代码注释。


Recall@K (R@K)

这是最早的也是最广为接受的指标,由卢老师在https://arxiv.org/abs/1608.00187中提出。因为VisualGenome数据集的ground-truth中对relationship的标注并不完整,所以简单的正确率并不能很好的反映SGG的效果。卢老师用了检索的指标Recall,把SGG看成检索问题,不仅要求识别准确,也要求能更好的剔除无关系的物体对。

给定一张图片,根据预测的主客体对<主体, 客体>和该主客体对所对应的每个关系类别的分数,按关系分数最高的选出该主客体对最佳的关系组成预测关系对<主体, 关系, 客体>。(也就是一个主客体对<主体, 客体>会组成一个预测关系对<主体, 关系, 客体>)

对前K个预测关系对(这K个预测关系对直接从前往后取,没有排序什么的操作),找到与其相匹配的真实关系对<主体, 关系, 客体>。K个预测对所对应的K个真实对的并集个数(就是这些真实对全部记录下来,但是排除掉重复的)记为m。每张图片有它已经标注好的真实对,这些真实对的个数记为n。每张图片的R@K就是:
R @ K = m n R@K=\frac{m}{n} R@K=nm
注意: 预测对和真实对间的匹配问题是主体一样、关系一样、客体一样才算匹配。

    def calculate_recall(self, global_container, local_container, mode):
        pred_rel_inds = local_container['pred_rel_inds']
        rel_scores = local_container['rel_scores']
        gt_rels = local_container['gt_rels']
        gt_classes = local_container['gt_classes']
        gt_boxes = local_container['gt_boxes']
        pred_classes = local_container['pred_classes']
        pred_boxes = local_container['pred_boxes']
        obj_scores = local_container['obj_scores']

        iou_thres = global_container['iou_thres']
		
		# [主体序号, 客体序号, 最匹配的关系序号]
        pred_rels = np.column_stack((pred_rel_inds, 1+rel_scores[:,1:].argmax(1)))
        # 每个预测关系对应的关系分数
        pred_scores = rel_scores[:,1:].max(1)

        gt_triplets, gt_triplet_boxes, _ = _triplet(gt_rels, gt_classes, gt_boxes)
        local_container['gt_triplets'] = gt_triplets
        local_container['gt_triplet_boxes'] = gt_triplet_boxes

        pred_triplets, pred_triplet_boxes, pred_triplet_scores = _triplet(
                pred_rels, pred_classes, pred_boxes, pred_scores, obj_scores)

        # Compute recall. It's most efficient to match once and then do recall after
        pred_to_gt = _compute_pred_matches(
            gt_triplets,
            pred_triplets,
            gt_triplet_boxes,
            pred_triplet_boxes,
            iou_thres,
            phrdet=mode=='phrdet',
        )
        local_container['pred_to_gt'] = pred_to_gt

        for k in self.result_dict[mode + '_recall']:
            # the following code are copied from Neural-MOTIFS
            match = reduce(np.union1d, pred_to_gt[:k])
            rec_i = float(len(match)) / float(gt_rels.shape[0])
            self.result_dict[mode + '_recall'][k].append(rec_i)

        return local_container

No Graph Constraint Recall@K (ngR@K)

这个指标最早由Pixel2Graph使用,由Neural-MOTIFS命名。这个指标的目的在于,传统的Recall计算里,一对物体只能有一个relation参与最终的排序,但ngR@K允许一对物体的所有relation都能参与排序。这也非常有道理,比如 human(0.9) - riding (0.6) - horse (0.9):total score=0.9x0.6x0.9,可能这对物体还有另一个relation:human(0.9) - on (0.3) - horse (0.9):total score=0.9x0.3x0.9。后者虽然分数比riding低,但也是一种可能的情况。ngR@K的结果往往大大高于单纯的R@K。

No Graph Constraint 与 Graph Constraint 不同在于预测关系对的选取方式不同,如下:
给定一张图片,预测的所有主客体对<主体, 客体>分别与该主客体对所对应的每个关系类别的分数相乘(即:主体分数 * 客体分数 * 关系分数。假设关系类别数为k,一个主客体对<主体, 客体>会组成k个预测关系对<主体, 关系, 客体>)。所有预测关系对按主体分数 * 客体分数 * 关系分数从大到小进行排序。

取前K个预测关系对,找到与其相匹配的真实关系对<主体, 关系, 客体>,这些真实关系对的并集个数(就是这些真实关系对全部记录下来,但是排除掉重复的)记为m。每张图片有它已经标注好的真实对,这些真实关系对的个数记为n。每张图片的R@K就是:
n g R @ K = m n ngR@K=\frac{m}{n} ngR@K=nm

注意: No Graph Constraint 还可以用在接下来介绍的mR@K和zR@K评估指标,而不止R@K。

    def calculate_recall(self, global_container, local_container, mode):
        obj_scores = local_container['obj_scores']
        pred_rel_inds = local_container['pred_rel_inds']
        rel_scores = local_container['rel_scores']
        pred_boxes = local_container['pred_boxes']
        pred_classes = local_container['pred_classes']
        gt_rels = local_container['gt_rels']
		
		# 每对关系的主语和宾语的分数相乘
        obj_scores_per_rel = obj_scores[pred_rel_inds].prod(1)
        # 主语和宾语分数的乘积与它们对应的每个关系类别的分数相乘
        nogc_overall_scores = obj_scores_per_rel[:,None] * rel_scores[:,1:]
        # 找出总乘积分数前一百对关系索引(主体客体可以单独重复)
        nogc_score_inds = argsort_desc(nogc_overall_scores)[:100]
        # 前一百对关系的主客体和最高分的类别 [主体序号, 客体序号, 分数最高的关系序号]
        nogc_pred_rels = np.column_stack((pred_rel_inds[nogc_score_inds[:,0]], nogc_score_inds[:,1]+1))
        # 前一百对关系的关系分数从高到低排序
        nogc_pred_scores = rel_scores[nogc_score_inds[:,0], nogc_score_inds[:,1]+1]

        nogc_pred_triplets, nogc_pred_triplet_boxes, _ = _triplet(
                nogc_pred_rels, pred_classes, pred_boxes, nogc_pred_scores, obj_scores
        )

        # No Graph Constraint
        gt_triplets = local_container['gt_triplets']
        gt_triplet_boxes = local_container['gt_triplet_boxes']
        iou_thres = global_container['iou_thres']

        nogc_pred_to_gt = _compute_pred_matches(
            gt_triplets,
            nogc_pred_triplets,
            gt_triplet_boxes,
            nogc_pred_triplet_boxes,
            iou_thres,
            phrdet=mode=='phrdet',
        )

        local_container['nogc_pred_to_gt'] = nogc_pred_to_gt

        for k in self.result_dict[mode + '_recall_nogc']:
            match = reduce(np.union1d, nogc_pred_to_gt[:k])
            rec_i = float(len(match)) / float(gt_rels.shape[0])
            self.result_dict[mode + '_recall_nogc'][k].append(rec_i)

        return local_container

Mean Recall@K (mR@K)

这个指标由我的VCTree和另外一个同学的KERN在2019年的CVPR中同时提出,不过我并没有作为VCTree的主要贡献,只在补充材料中完整展示了结果表。由于VisualGenome数据集的长尾效应,传统Recall往往只要学会几个主要的relation类比如on,near等,即便完全忽视大部分类别也可以取得很好的结果。这当然不是我们想看到的,所以mean Recall做了一件很简单的事,把所有谓语类别的Recall单独计算,然后求均值,这样所有类别就一样重要了。模型的驱动也从学会 尽可能多个 relation(有大量简单relation的重复)变成学会 尽可能多种类 的relation。

给定一张图片,设关系类别数量为n。对于每一个关系谓词类别,计算在第i类别下的R@K(R@K计算如上),然后对所有类别下的R@K取平均,公式如下:

m R @ K = ∑ 1 n R @ K i n mR@K=\frac{\sum_1^n{R@K_i}}{n} mR@K=n1nR@Ki

    def collect_mean_recall_items(self, global_container, local_container, mode):
        pred_to_gt = local_container['pred_to_gt']
        gt_rels = local_container['gt_rels']

        for k in self.result_dict[mode + '_mean_recall_collect']:
            # the following code are copied from Neural-MOTIFS
            match = reduce(np.union1d, pred_to_gt[:k])
            # NOTE: by kaihua, calculate Mean Recall for each category independently
            # this metric is proposed by: CVPR 2019 oral paper "Learning to Compose Dynamic Tree Structures for Visual Contexts"
            recall_hit = [0] * self.num_rel
            recall_count = [0] * self.num_rel
            '''
            统计该图片的所有真实关系对中出现的每一种关系类别的次数
            '''
            for idx in range(gt_rels.shape[0]):
                local_label = gt_rels[idx,2]
                recall_count[int(local_label)] += 1
                recall_count[0] += 1
			
			'''
            统计该图片的所有有匹配到真实关系对的预测关系对中出现的每一种关系类别的次数
            '''
            for idx in range(len(match)):
                local_label = gt_rels[int(match[idx]),2]
                recall_hit[int(local_label)] += 1
                recall_hit[0] += 1
            
            '''
            对每个关系类别进行统计,对每个关系类别计算该类别下预测关系对的召回率
            '''
            for n in range(self.num_rel):
                if recall_count[n] > 0:
                    self.result_dict[mode + '_mean_recall_collect'][k][n].append(float(recall_hit[n] / recall_count[n]))

Zero Shot Recall@K (zR@K)

在早期的视觉关系识别中,人们也使用了Zero Shot Recall指标,但在SGG中又渐渐被人忽视了,我们在这又重新增加了这个指标,因为它可以很好的展示SGG的拓展能力。Zero Shot Recall指的并不是从来没见过的relation,而只是在training中没见过的主语-谓语-宾语的三元组组合,所有单独的object和relation类别还是都见过的,不然就没法学了。

根据作者给定的说法,ZeroShot关系对表示训练中没出现过的<主, 谓, 宾>三元组组合。

给定一张图片,首先要找到没在训练中出现过的真实关系对,这里称为真实zeroshot关系对,数量记为n。对前K个预测关系对,找到与其相匹配的真实ZeroShot关系对<主体, 关系, 客体>。K个预测对所对应的K个真实ZeroShot关系对的并集个数(就是这些真实对全部记录下来,但是排除掉重复的)记为m。每张图片的zR@K就是:
z R @ K = m n zR@K=\frac{m}{n} zR@K=nm

def prepare_zeroshot(self, global_container, local_container):
    # 123
    gt_rels = local_container['gt_rels']
    gt_classes = local_container['gt_classes']
    # 这个就是作者所指的训练中没见过的<主, 谓, 宾>三元组组合
    zeroshot_triplets = global_container['zeroshot_triplet']

    sub_id, ob_id, pred_label = gt_rels[:, 0], gt_rels[:, 1], gt_rels[:, 2]
    gt_triplets = np.column_stack((gt_classes[sub_id], gt_classes[ob_id], pred_label))  # num_rel, 3
	
	'''
	对真实关系对进行检查,如果某一真实关系对在zeroshot关系对(这是预先写到文件上的)出现,
	则记录下这个真实关系对的序号
	'''
    self.zeroshot_idx = np.where( intersect_2d(gt_triplets, zeroshot_triplets).sum(-1) > 0 )[0].tolist()

def calculate_recall(self, global_container, local_container, mode):
    pred_to_gt = local_container['pred_to_gt']

    for k in self.result_dict[mode + '_zeroshot_recall']:
        # Zero Shot Recall
        # 真实关系对中有匹配到预测关系对的序号,记录的是真实关系对的序号
        match = reduce(np.union1d, pred_to_gt[:k])
        # 如果该图片中某一真实关系对在zeroshot关系对中出现
        if len(self.zeroshot_idx) > 0:
        	# 如果match的类型不是list和tuple类型,将它转换为列表
            if not isinstance(match, (list, tuple)):
                match_list = match.tolist()
            else:
                match_list = match
            # zeroshot_match: 预测关系对匹配到真实关系对中zeroshot关系对的数量
            zeroshot_match = len(self.zeroshot_idx) + len(match_list) - len(set(self.zeroshot_idx + match_list))
            zero_rec_i = float(zeroshot_match) / float(len(self.zeroshot_idx))
            self.result_dict[mode + '_zeroshot_recall'][k].append(zero_rec_i)

Top@K Accuracy (A@K)

这个是不好的评估方法!

这个指标来自于某个之前研究者对PredCls和SGCls的误解,并不建议大家report到文章中,这里列出来是希望大家以后别犯这个错。该同学在PredCls和SGCls中不仅给了所有object的bounding box,还给了主语-宾语所有pair的组合,所以这就完全不是一个Recall的检索了,而是给定两个物体,来判断他们relation的正确率。

在PredCls和SGCls中, 使用Top@K Accuracy (A@K)来报告成Recall ,这又是另一个误区。因为PredCls和SGCls中给定的只是所有object的bounding box,而非具体的主语-宾语的pair信息。一旦给定了pair信息,那么其实就没有recall的ranking了,只是纯粹的accuracy。这个误区最初发现于contrastive loss中。我花了小半个月才发现为什么他的PredCls和SGCls结果这么好。 它的症状也很简单,PredCls和SGCls中的Recall@50, Recall@100结果一摸一样 ,只有Recall@20稍低。因为没有了ranking,top50和100也就没区别了,没有图片有多于50个的ground-truth relationships。

作者在这里点名的论文叫做 Graphical Contrastive Losses for Scene Graph Parsing

给定一张图片,对所有的预测关系对<主体, 关系, 客体>,只要这些预测关系对中的主体和客体(不含关系)在某一真实关系对中同时出现,设标记为True。找到与标记为True的预测关系对相匹配的真实关系对<主体, 关系, 客体>,这些真实关系对的并集个数(就是这些真实关系对全部记录下来,但是排除掉重复的)记为m。每张图片有它已经标注好的真实对,这些真实关系对的个数记为n。每张图片的R@K就是:
A @ K = m n A@K=\frac{m}{n} A@K=nm

作者意思的个人理解:

一旦给定了pair信息,那么其实就没有recall的ranking了,只是纯粹的accuracy。

所谓recall,是召回率的意思,召回率的计算依据的是预测的和真实的完整关系对间的是否匹配,而不是主体客体间的匹配。这个方法是给定所有匹配的主体-客体对,召回率排名肯定不能仅仅依据完整关系对的其中一部分来算。

而是给定两个物体,来判断他们relation的正确率。

给定和真实关系对具有相同主体客体的预测关系对,那么只剩下关系能和真实关系对不一样。这些预测关系对,如果和真实关系对具有不一样的关系谓词,那么匹配的结果就是为空。

使用Top@K Accuracy (A@K)来报告成Recall

这个评估指标本身来说我觉得倒是还好,作者吐槽的点应该是把它当成recall导致recall大幅提升。

    def calculate_recall(self, global_container, local_container, mode):
        pred_to_gt = local_container['pred_to_gt']
        gt_rels = local_container['gt_rels']

        for k in self.result_dict[mode + '_accuracy_hit']:
            # to calculate accuracy, only consider those gt pairs
            # This metric is used by "Graphical Contrastive Losses for Scene Graph Parsing" 
            # for sgcls and predcls
            if mode != 'sgdet':
                gt_pair_pred_to_gt = []
                '''
                pred_to_gt 表示预测关系对<主体, 关系, 客体>在真实关系对中是否出现,如果出现则该项为
                	该预测关系对对应真实关系对的序号。
                self.pred_pair_in_gt 表示预测主客体对<主体, 客体>在真实关系对中是否出现(不考虑关系是否一致),
                	如果出现则该项为True,否则为False。
                '''
                for p, flag in zip(pred_to_gt, self.pred_pair_in_gt):
                    if flag:
                        gt_pair_pred_to_gt.append(p)
                if len(gt_pair_pred_to_gt) > 0:
                    gt_pair_match = reduce(np.union1d, gt_pair_pred_to_gt[:k])
                else:
                    gt_pair_match = []
                self.result_dict[mode + '_accuracy_hit'][k].append(float(len(gt_pair_match)))
                self.result_dict[mode + '_accuracy_count'][k].append(float(gt_rels.shape[0]))


参考:

KaihuaTang/Scene-Graph-Benchmark.pytorch
CVPR2020 | 最新最完善的场景图生成 (SGG) 框架,集成目前最全 metrics,已开源

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值