一、程序结构展示
这次的笔记主要介绍utils文件下的“metrics.py”文件的内容。
二、代码功能介绍
这段代码是一个用于评估机器学习模型性能的工具函数集合。
-
calculate_max_f1(gt, scores)
: 这个函数用于计算给定真实标签和预测分数的最大 F1 分数及其对应的阈值。它首先使用precision_recall_curve
函数计算出一系列不同阈值下的精确率和召回率,然后根据 F1 分数的定义计算 F1 分数,最后返回最大 F1 分数和对应的阈值。 -
metric_cal(scores, gt_list, gt_mask_list, cal_pro=False)
: 这个函数用于计算模型的多个性能指标,包括图像级 ROC AUC 分数、图像级 F1 分数、像素级 ROC AUC 分数、像素级 F1 分数等。根据参数cal_pro
的取值,它还可以计算额外的指标。该函数的返回结果是一个包含各项性能指标值的字典。 -
rescale(x)
: 这个函数用于对输入数组进行归一化,将数组中的值缩放到 [0, 1] 的范围内。 -
cal_pro_metric(labeled_imgs, score_imgs, fpr_thresh=0.3, max_steps=200)
: 这个函数用于计算基于像素级别预测结果的平均 IoU 分数(Intersection over Union)以及 Pro AUC 分数。它通过比较预测结果和真实标签之间的重叠来计算 IoU 分数,并根据不同的 FPR 阈值计算 Pro AUC 分数。 -
calculate_max_f1_region(labeled_imgs, score_imgs, pro_thresh=0.6, max_steps=200)
: 这个函数用于计算基于区域级别预测结果的最大 F1 分数。它首先将预测结果二值化,然后对每个区域计算 F1 分数,并返回最大 F1 分数。
这些函数主要用于评估二分类模型在图像分割任务中的性能,涉及到常见的评价指标如 ROC AUC、F1 分数、IoU 等。
三、代码逐行注释
import numpy as np
from skimage import measure # scikit-image是Python中的图像处理库,提供了丰富的图像处理功能。这行代码引入了包含了一些用于测量图像特征和属性的函数
from sklearn.metrics import auc # scikit-learn是Python中用于机器学习的库,其中metrics模块包含了各种用于评估模型性能的函数。auc函数用于计算ROC曲线下的面积(AUC,Area Under Curve)
from sklearn.metrics import precision_recall_curve # precision_recall_curve函数用于计算在不同阈值下的精确度-召回率曲线。
from sklearn.metrics import roc_auc_score # roc_auc_score函数用于计算ROC曲线下的面积,即ROC AUC
from sklearn.metrics import roc_curve # roc_curve函数用于计算ROC曲线上的点,即假阳率和真阳率。
def calculate_max_f1(gt, scores): # 实现了计算F1值的函数,F1值是精确率(Precision)和召回率(Recall)的调和平均,用于评估二分类模型的性能。
precision, recall, thresholds = precision_recall_curve(gt, scores) # 这一行计算了给定真实标签(gt)和模型预测得分(scores)的精确率、召回率以及相应的阈值。precision_recall_curve是一个scikit-learn中的函数,用于计算不同阈值下的精确率和召回率。
a = 2 * precision * recall # F1值的分子(2 * precision * recall)
b = precision + recall # 分母(precision + recall)
f1s = np.divide(a, b, out=np.zeros_like(a), where=b != 0) # 使用NumPy的divide函数计算F1值。它首先进行了分母非零的检查(where=b != 0),以避免除以零的情况。如果分母为零,则将F1值设置为零。这里使用out=np.zeros_like(a)参数指定了输出数组的形状和类型,保证了结果的一致性。
index = np.argmax(f1s) # 这一行找到了F1值数组中最大值的索引,即找到了最佳F1值所对应的阈值的索引
max_f1 = f1s[index] # 获取了最佳F1值和对应的阈值
threshold = thresholds[index]
return max_f1, threshold
def metric_cal(scores, gt_list, gt_mask_list, cal_pro=False): # 实现了计算评估指标的函数,主要用于评估二分类模型的性能
# calculate image-level ROC AUC score
img_scores = scores.reshape(scores.shape[0], -1).max(axis=1) # 将模型的输出得分(scores)转换为一维数组,并且对每个样本取最大值,得到了图像级别的预测得分(img_scores)
gt_list = np.asarray(gt_list, dtype=int) # 将真实标签(gt_list)转换为NumPy数组,并确保其数据类型为整数型
fpr, tpr, _ = roc_curve(gt_list, img_scores) # 使用roc_curve函数计算了图像级别的ROC曲线的假阳率(fpr)和真阳率(tpr),以及对应的阈值(_)
img_roc_auc = roc_auc_score(gt_list, img_scores) # 计算了图像级别的ROC曲线下的面积(ROC AUC),用于评估模型在整个数据集上的性能
# print('INFO: image ROCAUC: %.3f' % (img_roc_auc))
img_f1, img_threshold = calculate_max_f1(gt_list, img_scores) # 调用了calculate_max_f1函数,计算了图像级别的最佳F1值和对应的阈值
gt_mask = np.asarray(gt_mask_list, dtype=int) # 将真实标签的掩码列表(gt_mask_list)转换为NumPy数组,并确保其数据类型为整数型
pxl_f1, pxl_threshold = calculate_max_f1(gt_mask.flatten(), scores.flatten()) # 调用了calculate_max_f1函数,计算了像素级别的最佳F1值和对应的阈值。首先将真实标签的掩码展平为一维数组,然后与模型的输出得分也展平后一起传入函数
# calculate per-pixel level ROCAUC
fpr, tpr, _ = roc_curve(gt_mask.flatten(), scores.flatten()) # 再次使用roc_curve函数计算了像素级别的ROC曲线的假阳率(fpr)和真阳率(tpr),以及对应的阈值(_)
per_pixel_rocauc = roc_auc_score(gt_mask.flatten(), scores.flatten()) # 计算了像素级别的ROC曲线下的面积(ROC AUC),用于评估模型在像素级别上的性能
# 两个条件判断语句用于决定是否计算像素级别的Pro AUC和最大F1值区域。如果cal_pro参数为True,则计算Pro AUC和最大F1值区域,否则将它们的值设置为0
if cal_pro:
pro_auc_score = cal_pro_metric(gt_mask_list, scores, fpr_thresh=0.3)
# calculate max-f1 region
max_f1_region = calculate_max_f1_region(gt_mask_list, scores)
else:
pro_auc_score = 0
# calculate max-f1 region
max_f1_region = 0
# 这一行创建了一个字典,其中包含了计算得到的评估指标的值。具体包括图像级别的ROC AUC('i_roc')、像素级别的ROC AUC('p_roc')、像素级别的Pro AUC('p_pro')、图像级别的F1值('i_f1')、像素级别的F1值('p_f1')和最大F1值区域('r_f1')
result_dict = {'i_roc': img_roc_auc * 100, 'p_roc': per_pixel_rocauc * 100, 'p_pro': pro_auc_score * 100,
'i_f1': img_f1 * 100, 'p_f1': pxl_f1 * 100, 'r_f1': max_f1_region * 100}
return result_dict
def rescale(x): # 用于归一化的函数,将输入数组x中的值重新缩放到[0, 1]的范围内
return (x - x.min()) / (x.max() - x.min())
def cal_pro_metric(labeled_imgs, score_imgs, fpr_thresh=0.3, max_steps=200):
# 这是一个用于计算pro_auc_score的函数。它接受以下参数:
# labeled_imgs:标记的图像,通常是二进制掩模或标签图像。
# score_imgs:模型对图像进行预测后生成的得分图像。
# fpr_thresh:false positive rate(FPR)的阈值,默认为0.3。
# max_steps:计算过程中的最大步数,默认为200。
labeled_imgs = np.array(labeled_imgs) # 将labeled_imgs转换为NumPy数组,并将其二值化(小于等于0.45的值设为0,大于0.45的值设为1)
labeled_imgs[labeled_imgs <= 0.45] = 0
labeled_imgs[labeled_imgs > 0.45] = 1
labeled_imgs = labeled_imgs.astype(np.bool)
max_th = score_imgs.max() # 计算得分图像的最大值和最小值,然后根据最大值和最小值以及max_steps计算步长delta
min_th = score_imgs.min()
delta = (max_th - min_th) / max_steps
ious_mean = [] # 初始化一些空列表来存储计算结果
ious_std = [] # iou(intersection over union)、pros(每个区域的重叠比例)、fprs(false positive rates)
pros_mean = []
pros_std = []
threds = []
fprs = []
binary_score_maps = np.zeros_like(score_imgs, dtype=bool)
for step in range(max_steps): # 在每个步骤中,根据当前阈值将得分图像二值化,并计算出每个区域的重叠比例和每个图像的iou
thred = max_th - step * delta
# segmentation
binary_score_maps[score_imgs <= thred] = 0
binary_score_maps[score_imgs > thred] = 1
pro = [] # per region overlap
iou = [] # per image iou
# pro: find each connected gt region, compute the overlapped pixels between the gt region and predicted region
# iou: for each image, compute the ratio, i.e. intersection/union between the gt and predicted binary map
for i in range(len(binary_score_maps)): # for i th image
# pro (per region level)
label_map = measure.label(labeled_imgs[i], connectivity=2)
props = measure.regionprops(label_map)
for prop in props:
x_min, y_min, x_max, y_max = prop.bbox
cropped_pred_label = binary_score_maps[i][x_min:x_max, y_min:y_max]
# cropped_mask = masks[i][x_min:x_max, y_min:y_max]
cropped_mask = prop.filled_image # corrected!
intersection = np.logical_and(cropped_pred_label, cropped_mask).astype(np.float32).sum()
pro.append(intersection / prop.area)
# iou (per image level)
intersection = np.logical_and(binary_score_maps[i], labeled_imgs[i]).astype(np.float32).sum()
union = np.logical_or(binary_score_maps[i], labeled_imgs[i]).astype(np.float32).sum()
if labeled_imgs[i].any() > 0: # when the gt have no anomaly pixels, skip it
iou.append(intersection / union)
# against steps and average metrics on the testing data
ious_mean.append(np.array(iou).mean())
# print("per image mean iou:", np.array(iou).mean())
ious_std.append(np.array(iou).std())
pros_mean.append(np.array(pro).mean())
pros_std.append(np.array(pro).std())
# fpr for pro-auc
masks_neg = ~labeled_imgs
fpr = np.logical_and(masks_neg, binary_score_maps).sum() / masks_neg.sum()
fprs.append(fpr)
threds.append(thred)
# as array
threds = np.array(threds)
pros_mean = np.array(pros_mean)
pros_std = np.array(pros_std)
fprs = np.array(fprs)
# default 30% fpr vs pro, pro_auc
idx = fprs <= fpr_thresh # find the indexs of fprs that is less than expect_fpr (default 0.3)
fprs_selected = fprs[idx] # 根据设定的fpr_thresh,筛选出相应的FPR和pros_mean,并将FPR进行归一化
fprs_selected = rescale(fprs_selected) # rescale fpr [0,0.3] -> [0, 1]
pros_mean_selected = pros_mean[idx]
pro_auc_score = auc(fprs_selected, pros_mean_selected) # 使用auc函数计算归一化后的FPR和pros_mean之间的曲线下面积,得到pro_auc_score
# print("pro auc ({}% FPR):".format(int(expect_fpr * 100)), pro_auc_score)
return pro_auc_score
def calculate_max_f1_region(labeled_imgs, score_imgs, pro_thresh=0.6, max_steps=200):
# 计算模型在不同阈值下的最大 F1 分数及其对应的召回率和精确度,以评估模型在区域级别上的性能
# labeled_imgs:标记的图像,通常是二进制掩模或标签图像。
# score_imgs:模型对图像进行预测后生成的得分图像。
# pro_thresh:F1 分数的阈值,默认为0.6。
# max_steps:计算过程中的最大步数,默认为200
labeled_imgs = np.array(labeled_imgs) #
# labeled_imgs[labeled_imgs <= 0.1] = 0
# labeled_imgs[labeled_imgs > 0.1] = 1
labeled_imgs = labeled_imgs.astype(bool) # 将labeled_imgs转换为NumPy数组,并将其转换为布尔
max_th = score_imgs.max() # 计算得分图像的最大值和最小值,并根据最大值和最小值以及max_steps计算步长 delta
min_th = score_imgs.min()
delta = (max_th - min_th) / max_steps
f1_list = [] # 初始化空列表来存储 F1 分数、召回率和精确度
recall_list = []
precision_list = []
binary_score_maps = np.zeros_like(score_imgs, dtype=bool) # 初始化二值化后的得分图像二维数组 binary_score_maps,作为预测的区域
for step in range(max_steps): # 对于每个阈值步骤
thred = max_th - step * delta
# segmentation
binary_score_maps[score_imgs <= thred] = 0 # 根据当前阈值将得分图像二值化为二进制预测区域
binary_score_maps[score_imgs > thred] = 1
pro = [] # per region overlap
predict_region_number = 0
gt_region_number = 0
# pro: find each connected gt region, compute the overlapped pixels between the gt region and predicted region
# iou: for each image, compute the ratio, i.e. intersection/union between the gt and predicted binary map
for i in range(len(binary_score_maps)): # for i th image
# pro (per region level)
label_map = measure.label(labeled_imgs[i], connectivity=2) # 计算标记图像的连通区域并遍历每个区域
props = measure.regionprops(label_map)
score_map = measure.label(binary_score_maps[i], connectivity=2)
score_props = measure.regionprops(score_map)
predict_region_number += len(score_props)
gt_region_number += len(props)
# if len(score_props) == 0 or len(props) == 0:
# pro.append(0)
# continue
for score_prop in score_props: # 计算预测区域与标记区域的交集与并集,并计算重叠比例(intersection over union)
x_min_0, y_min_0, x_max_0, y_max_0 = score_prop.bbox
cur_pros = [0]
for prop in props:
x_min_1, y_min_1, x_max_1, y_max_1 = prop.bbox
x_min = min(x_min_0, x_min_1)
y_min = min(y_min_0, y_min_1)
x_max = max(x_max_0, x_max_1)
y_max = max(y_max_0, y_max_1) # 将每个区域的最大重叠比例存储在列表 pro 中
cropped_pred_label = binary_score_maps[i][x_min:x_max, y_min:y_max]
cropped_gt_label = labeled_imgs[i][x_min:x_max, y_min:y_max]
# cropped_mask = masks[i][x_min:x_max, y_min:y_max]
# cropped_mask = prop.filled_image # corrected!
intersection = np.logical_and(cropped_pred_label, cropped_gt_label).astype(np.float32).sum()
union = np.logical_or(cropped_pred_label, cropped_gt_label).astype(np.float32).sum()
cur_pros.append(intersection / union)
pro.append(max(cur_pros))
pro = np.array(pro)
if gt_region_number == 0 or predict_region_number == 0:
print(f'gt_number: {gt_region_number}, pred_number: {predict_region_number}')
recall = 0
precision = 0
f1 = 0
else:
recall = np.array(pro >= pro_thresh).astype(np.float32).sum() / gt_region_number
precision = np.array(pro >= pro_thresh).astype(np.float32).sum() / predict_region_number
if recall == 0 or precision == 0:
f1 = 0
else:
f1 = 2 * recall * precision / (recall + precision)
f1_list.append(f1)
recall_list.append(recall)
precision_list.append(precision)
# as array
f1_list = np.array(f1_list) # 计算整个数据集的召回率、精确度和 F1 分数
max_f1 = f1_list.max()
cor_recall = recall_list[f1_list.argmax()]
cor_precision = precision_list[f1_list.argmax()]
print(f'cor recall: {cor_recall}, cor precision: {cor_precision}')
return max_f1