行人重识别02-10:fast-reid(BoT)-pytorch编程规范(fast-reid为例)7-模型测试评估-2

以下链接是个人关于fast-reid(BoT行人重识别) 所有见解,如有错误欢迎大家指出,我会第一时间纠正。有兴趣的朋友可以加微信:a944284742相互讨论技术。若是帮助到了你什么,一定要记得点赞!因为这是对我最大的鼓励。
行人重识别02-00:fast-reid(BoT)-目录-史上最新无死角讲解

前言

在上篇博客中提到 fastreid\evaluation\evaluator.py 中函数 inference_on_dataset 由两个比较重要的地方:

    # 把提出的特征,以及标签添加到evaluator.pids,evaluator.camids,evaluator.features之中
    evaluator.process(inputs, outputs)
    # 对结果进行评估
    results = evaluator.evaluate()

其中都涉及到了 evaluator ,那么我们现在就来看看,其到底是何方神圣。在tools\train_net.py文件中可以看到:

# 继承于DefaultTrainer创建一个训练类
class Trainer(DefaultTrainer):
    @classmethod # 重写评估函数
    def build_evaluator(cls, cfg, num_query, output_folder=None):
        """
        :param cfg: 配置参数
        :param num_query: 测试数据集的数目
        :param output_folder: 输出目录
        :return:
        """
        if output_folder is None:
            output_folder = os.path.join(cfg.OUTPUT_DIR, "inference")
        return ReidEvaluator(cfg, num_query)

这里的 ReidEvaluator,就是我们想要了解的东西,上面的 evaluator 就是其创建出来的类对象。那么我们就具体去看看其实现了那些东西。

ReidEvaluator

ReidEvaluator的注释如下:

class ReidEvaluator(DatasetEvaluator):
    def __init__(self, cfg, num_query, output_dir=None):
        # 获得配置信息
        self.cfg = cfg
        # 获得需要查询图片的数目
        self._num_query = num_query
        # 获得数目目录
        self._output_dir = output_dir
        # 用于存储提取的特征
        self.features = []
        # 用于存储身份id
        self.pids = []
        # 用于存储图片对应的摄像头id
        self.camids = []

    def reset(self):
        # 把身份特征,身份id,图片对应的摄像头id全部清零
        self.features = []
        self.pids = []
        self.camids = []

    def process(self, inputs, outputs):
        # 获得query图片的身份id,以及摄像头ID,outputs(图片提出出来的features)。因为使用的是extend函数,
        # 最后会把query中的所有图片信息都分别添加到 self.pids,self.camids,self.features之中
        self.pids.extend(inputs["targets"])
        self.camids.extend(inputs["camid"])
        self.features.append(outputs.cpu())

    @staticmethod
    def cal_dist(metric: str, query_feat: torch.tensor, gallery_feat: torch.tensor):
        assert metric in ["cosine", "euclidean"], "must choose from [cosine, euclidean], but got {}".format(metric)
        if metric == "cosine":
            dist = 1 - torch.mm(query_feat, gallery_feat.t())
        else:
            m, n = query_feat.size(0), gallery_feat.size(0)
            xx = torch.pow(query_feat, 2).sum(1, keepdim=True).expand(m, n)
            yy = torch.pow(gallery_feat, 2).sum(1, keepdim=True).expand(n, m).t()
            dist = xx + yy
            dist.addmm_(query_feat, gallery_feat.t(), beta=1, alpha=-2)
            dist = dist.clamp(min=1e-12).sqrt()  # for numerical stability
        return dist.cpu().numpy()

    def evaluate(self):
        """
        对提取到的特征进行评估
        """
        # 使用分布式进行计算
        if comm.get_world_size() > 1:
            comm.synchronize()
            features = comm.gather(self.features)
            features = sum(features, [])

            pids = comm.gather(self.pids)
            pids = sum(pids, [])

            camids = comm.gather(self.camids)
            camids = sum(camids, [])

            if not comm.is_main_process():
                return {}
        # 非分布式,单台机器进行计算
        else:
            features = self.features
            pids = self.pids
            camids = self.camids
        # 把所有特征垂直连接起来,
        features = torch.cat(features, dim=0)
        # query feature, person ids and camera ids
        # query 和 bounding_box_test 中的数据是串接起来的,现在把他们独立分开来
        # query 表示需要被查询的数据
        query_features = features[:self._num_query]
        query_pids = np.asarray(pids[:self._num_query])
        query_camids = np.asarray(camids[:self._num_query])
        # gallery features, person ids and camera ids
        # gallery表示 bounding_box_test
        gallery_features = features[self._num_query:]
        gallery_pids = np.asarray(pids[self._num_query:])
        gallery_camids = np.asarray(camids[self._num_query:])

        # 用于保存结果
        self._results = OrderedDict()

        # 如果设置了AQE,则进行aqe处理:将检索到的 top k最近邻与原始查询合并,并执行另一次检索
        if self.cfg.TEST.AQE.ENABLED:
            logger.info("Test with AQE setting")
            qe_time = self.cfg.TEST.AQE.QE_TIME
            qe_k = self.cfg.TEST.AQE.QE_K
            alpha = self.cfg.TEST.AQE.ALPHA
            query_features, gallery_features = aqe(query_features, gallery_features, qe_time, qe_k, alpha)

        # 如果设置了评估标准为 cosine 则对 query_features 与 gallery_features 进行正则化处理
        if self.cfg.TEST.METRIC == "cosine":
            query_features = F.normalize(query_features, dim=1)
            gallery_features = F.normalize(gallery_features, dim=1)

        # 计算欧式距离或者余弦距离:
        # [3368,2048](query提取的特征向量), [15913,2048](bounding_box_test提取的特征向量)-->[3368,15913]
        dist = self.cal_dist(self.cfg.TEST.METRIC, query_features, gallery_features)

        # 如果使能了RERANK,默认为False
        if self.cfg.TEST.RERANK.ENABLED:
            logger.info("Test with rerank setting")
            k1 = self.cfg.TEST.RERANK.K1
            k2 = self.cfg.TEST.RERANK.K2
            lambda_value = self.cfg.TEST.RERANK.LAMBDA
            q_q_dist = self.cal_dist(self.cfg.TEST.METRIC, query_features, query_features)
            g_g_dist = self.cal_dist(self.cfg.TEST.METRIC, gallery_features, gallery_features)
            re_dist = re_ranking(dist, q_q_dist, g_g_dist, k1, k2, lambda_value)
            # 把query_features,gallery_features转换为numpy到cpu上
            query_features = query_features.numpy()
            gallery_features = gallery_features.numpy()
            cmc, all_AP, all_INP = evaluate_rank(re_dist, query_features, gallery_features,
                                                 query_pids, gallery_pids, query_camids,
                                                 gallery_camids, use_distmat=True)
        else:
            # 把query_features,gallery_features转换为numpy到cpu上
            query_features = query_features.numpy()
            gallery_features = gallery_features.numpy()

            # 进行rank评估:dist表示测试出来的欧式距离,或者余弦相似度
            # query_features表示query图片对应的特征向量,gallery_features表示bounding_box_test中图像对应的特征向量
            # query_pids 表示query图片对应的身份ID, gallery_pids 表示 bounding_box_test中图像对应的身份ID
            # query_camids 表示query图片对应的摄像头id,gallery_camids 表示 bounding_box_test中图像对应的摄像头id
            cmc, all_AP, all_INP = evaluate_rank(dist, query_features, gallery_features,
                                                 query_pids, gallery_pids, query_camids, gallery_camids,

                                                 use_distmat=False)
        mAP = np.mean(all_AP)
        mINP = np.mean(all_INP)
        for r in [1, 5, 10]:
            self._results['Rank-{}'.format(r)] = cmc[r - 1]
        self._results['mAP'] = mAP
        self._results['mINP'] = mINP

        # 如果是能了ROC,则进行ROC评估
        if self.cfg.TEST.ROC_ENABLED:
            scores, labels = evaluate_roc(dist, query_features, gallery_features,
                                          query_pids, gallery_pids, query_camids, gallery_camids)
            fprs, tprs, thres = metrics.roc_curve(labels, scores)

            for fpr in [1e-4, 1e-3, 1e-2]:
                ind = np.argmin(np.abs(fprs - fpr))
                self._results["TPR@FPR={:.0e}".format(fpr)] = tprs[ind]

        return copy.deepcopy(self._results)

其上的代码,主要进行了两种评估方式:

# rank评估
cmc, all_AP, all_INP = evaluate_rank(re_dist, query_features,gallery_features,query_pids, gallery_pids,query_camids,gallery_camids, use_distmat=True)
# roc评估
scores, labels = evaluate_roc(dist, query_features, gallery_features,query_pids, gallery_pids, query_camids, gallery_camids)

本人只对 rank评估方式进行讲解,对于 roc 的评估,有需要了解的同志,可以自己琢磨一下代码。

evaluate_rank

在对代码进行了解之前,我们需要了解一下cmc评估的概念,这是我参考的一篇博客:
https://blog.csdn.net/weixin_40446557/article/details/83106995
对于 evaluate_rank 这个函数,我们把参数设置 use_cython=False 之后,其最终调用到的是 fastreid\evaluation\rank.py 中的 def eval_market1501() 函数:


def eval_market1501(distmat, q_feats, g_feats, q_pids, g_pids, q_camids, g_camids, max_rank, use_distmat):
    """Evaluation with market1501 metric
    Key: for each query identity, its gallery images from the same camera view are discarded.
    """
    # 获得query,gallery 图片(特征)的数目
    num_q, num_g = distmat.shape
    # 获得特征向量的维度
    dim = q_feats.shape[1]
    # 为faiss指定每个特征向量的维度
    index = faiss.IndexFlatL2(dim)
    # 把g_feats添加到这个容器之中
    index.add(g_feats)

    # 如果gallery的数目小于max_rank则报错
    if num_g < max_rank:
        max_rank = num_g
        print('Note: number of gallery samples is quite small, got {}'.format(num_g))
    # 如果使用distmat方式,则对distmat(每一列)进行排序,获得排序之后的索引
    if use_distmat:
        indices = np.argsort(distmat, axis=1)
    # 如果不使用distmat方式,则通过 index.search 进行搜索
    else:
        _, indices = index.search(q_feats, k=num_g)

    # 进行匹配,如果g_pids[indices]等于q_pids[:, np.newaxis]身份ID,则被置1。
    # matches[3368,15913],排列之后的结果类似如下:
    #
    matches = (g_pids[indices] == q_pids[:, np.newaxis]).astype(np.int32)

    # compute cmc curve for each query,计算每个查询的cmc曲线
    all_cmc = []
    all_AP = [] # 记录 query 每张图像的AP
    all_INP = []
    num_valid_q = 0.  # number of valid query,记录有效的query数量

    # 循环处理 query 每张图片的结果
    for q_idx in range(num_q):
        # get query pid and camid,获得query的 pid 以及 camid
        q_pid = q_pids[q_idx]
        q_camid = q_camids[q_idx]

        # remove gallery samples that have the same pid and camid with query

        # 取出当前第q_idx张图片,在gallery中查询过后的排序结果
        # [3368,15913]-->[15913,]
        order = indices[q_idx]

        # 删除与查询具有相同pid和camid的gallery样本,也就是删除query和gallery中相同图片的结果
        remove = (g_pids[order] == q_pid) & (g_camids[order] == q_camid)
        keep = np.invert(remove)
        # compute cmc curve,二进制向量,值为1的位置是正确的匹配
        raw_cmc = matches[q_idx][keep]  # binary vector, positions with value 1 are correct matches
        if not np.any(raw_cmc):
            # 当查询标识未出现在图库中时,此条件为真
            # this condition is true when query identity does not appear in gallery
            continue
        # 计算一行中的累加值,如一个数组为[0,0,1,1,0,2,0]
        # 通过cumsum得到[0,0,1,2,2,4,4]
        cmc = raw_cmc.cumsum()
        # 获得raw_cmc中值为 1 的坐标
        pos_idx = np.where(raw_cmc == 1)
        # 选择最大的一个坐标
        max_pos_idx = np.max(pos_idx)
        # 计算inp,该值约大表示预测得约准
        inp = cmc[max_pos_idx] / (max_pos_idx + 1.0)
        all_INP.append(inp)

        # cmc > 1的位置,表示都预测正确了
        cmc[cmc > 1] = 1
        # 根据max_rank,添cmc到all_cmc之中
        all_cmc.append(cmc[:max_rank])
        num_valid_q += 1.

        # compute average precision,计算平均精度
        # reference: https://en.wikipedia.org/wiki/Evaluation_measures_(information_retrieval)#Average_precision
        num_rel = raw_cmc.sum()
        tmp_cmc = raw_cmc.cumsum()
        tmp_cmc = [x / (i + 1.) for i, x in enumerate(tmp_cmc)]
        tmp_cmc = np.asarray(tmp_cmc) * raw_cmc
        AP = tmp_cmc.sum() / num_rel
        all_AP.append(AP)

    # 所有查询身份没有出现在图库(gallery)中则报错
    assert num_valid_q > 0, 'Error: all query identities do not appear in gallery'

    # 计算平均cmc精度
    all_cmc = np.asarray(all_cmc).astype(np.float32)
    all_cmc = all_cmc.sum(0) / num_valid_q

    return all_cmc, all_AP, all_INP

结语

到这里,我们对于模型的评估已经有了一个大致的了解,那么在后面我会带大家去训练自己的数据。

你可能感兴趣的:(行人重识别,fast-reid,行人重识别,pytorch,BoT,ReID)