當在衡量模型的表現結果 (performance) 時,會藉由評估指標來進行,本文將要介紹在影像分割 (Image Segmentation) 任務上經常使用的評估指標,並進行實作,所有 code 會放置在文章下方。
Confusion Matrix
混淆矩陣 (Confusion Matrix) 是由 TP、FP、FN、TN 四種狀況所組合而成,可以很清楚地反映各類別之間被劃分的關係,並且藉由 confusion matrix 可以再延伸出其他評估指標。
來看一下 TP、FP、FN、TN 的定義,假設我們要預測的類別有兩個類別,分別為有病 (label=1) 以及沒病 (label=0)
- TP (True Positive,真陽性):該類別實際為有病,預測也為有病,表示檢測正確
- FP (False Positive,真陰性):該類別實際為沒有病,預測成有病,表示誤檢
- FN (False Negative,假陰性):該類別實際為有病,預測為沒有病,表示漏檢
- TN (True Negative,假陽性):該類別實際為沒有病,預測也為沒有病,即表示不需要被檢測的地方沒被檢測出來
接著,來試著實作看看吧!假設我們的圖像資料為 9 個 pixel,實際 label 如左圖 (稱為 Ground Truth)、預測 label 如右圖。
用程式碼表示如下
ground_truth = np.array([1, 1, 0, 1, 0, 0, 1, 1, 0])
pred_label = np.array([1, 0, 0, 1, 1, 0, 1, 1, 1])
- 計算 TP
先比對 Ground Truth 與 Predict 的每個 pixel,若該 pixel 滿足兩者相同的條件則賦值為 1、反之為 0,也就是要找出實際為真、預測也為真或是實際為假、預測也為假的 pixel。
接著將上述的輸出值與 Ground Truth 的每個 pixel 相乘再加總,由於 Ground Truth 為有病的 pixel 為 1,因此這樣的計算能得到所有 Ground Truth 為有病、Predict 也為有病的數量,即 TP (實際為真、預測也為真)。
### tp
# 所有實際與預測相同的值
gt_equal_pred = np.where(ground_truth==pred_label, 1, 0)
# 實際值與預測值相同,並且實際為真
tp = np.sum(gt_equal_pred * ground_truth)
- 計算 FP
從 confusion matrix 中可得知所有預測為真的數量為 TP 和 FP 的加總,因此可以先計算 pixel 預測為 1 的數量,再減去 TP。
### fp
# 所有預測為真的數量去除掉 TP
fp = np.sum(pred_label) - tp
- 計算 FN
一樣藉由 confusion matrix 得知所有實際為真的數量為 TP 和 FN 的加總,因此可以先計算 Ground Truth 中 pixel 為 1 的數量,再減去 TP。
### fn
# 所有實際為真的數量去除掉 TP
fn = np.sum(ground_truth) - tp
- 計算 TN
跟 TP 的計算一樣,要先找出實際與預測皆相同的 pixel。接著將所有 Ground Truth 為沒有病的 pixel 賦值為 1、反之為 0。最後將兩者的輸出相乘再加總,就能得到所有 Ground Truth 為沒有病、Predict 也沒有病的數量,即 TN (實際為假、預測也為假)。
### tn
# 所有實際與預測相同的值
gt_equal_pred = np.where(ground_truth==pred_label, 1, 0)
# 所有實際為假的值
gt_is_false = np.where(np.logical_not(ground_truth), 1, 0)
# 實際值與預測值相同,並且預測為假
tn = np.sum(gt_equal_pred * gt_is_false)
計算結果如下圖所示,粉紅框為 TP,數量為 4;紫色框為 FP,數量為 2;藍色框為 FN,數量為 1;沒有框的為 TN,數量為 2。
也可以使用 sklearn 來計算 confusion matrix
from sklearn.metrics import confusion_matrix
tn, fp, fn, tp = confusion_matrix(ground_truth, pred_label).ravel()
TP、FP、FN、TN
那 TP、FP、FN、TN 是怎麼得到的呢?我們知道模型預測的輸出表示為可能屬於各個類別的機率值,當某個類別的機率值越大,表示模型認為屬於該類別的可能性越大。接著我們需要從該機率值來判定要歸類給哪個類別,後就可以來計算各個類別的 TP、FP、FN、TN。
以二分類模型為例,預測是否有病。下圖左為圖像中每個 pixel 的預測結果,此時要設定一個閾值 (threshold) 來表示是否要將該 pixel 歸類為有病。
假設 threshold 設定為 0.5,若該 pixel 的預測結果大於 threshold,則將其歸類為有病,反之則歸類為沒病。
來看一下實際圖片在影像分割任務上的 TP、FP、FN、TN 會更清楚,左圖為 Ground Truth、右圖為 Prediction 結果。中間黃色區塊指 Ground Truth 為真、Prediction 也為真的狀況,表示為 TP;左邊藍色斜線指 Ground Truth 為假、Prediction 為真的狀況,表示為 FP;右邊紅色斜線指 Ground Truth 為真、Prediction 為假的狀況,表示為 FN;白色斜線 (背景) 指 Ground Truth 為假、Prediction 也為假的狀況,表示為 TN。
IoU (Intersection over Union)
IoU 是指 Ground Truth 與 Prediction 之間的交集除以聯集,由下圖的公式可以計算每個類別的 IoU。
以上述的例子:tp = 4、fn = 1、fp = 2、tn = 2 來計算 IoU。
iou = tp / (tp + fp + fn)
print("iou:", iou)
# ===================
iou: 0.5714285714285714
Precision、Recall
Precision 表示精確率, 定義為所有預測為真的數據中,實際也為真的機率;Recall 表示召回率,定義為所有實際為真的數據中,預測也為真的機率。兩者都是屬於關注於正樣本的指標,差別在於關注角度不同。
使用時機在於:當 FP 的代價很高,期望盡可能地降低誤檢,應著重提高 Precision;當 FN 的代價很高,期望盡可能地降低漏檢,應著重提高 Recall。
下圖為 Precision、Recall 的公式及在 confusion matrix 的關係。
接著一樣來進行實作,假設我們的圖像 Prediction 和 Ground Truth 如下圖,threshold 設定為 0.5。
藉由 threshold 以得到 Prediction 右邊的結果。
ground_truth = np.array([1, 1, 0, 1, 0, 0, 1, 1, 0])
pred_score = np.array([0.8, 0.4, 0.1, 0.7, 0.6, 0.2, 0.9, 0.8, 0.6])
threshold = 0.5
pred_label = np.where(pred_score > threshold, 1, 0)
print("pred_label:", pred_label)
# =======================
pred_label: [1 0 0 1 1 0 1 1 1]
將 Prediction 結果與 Ground Truth 進行 TP、FP、FN、TN 的計算後,就能夠計算 Precision、Recall。
precision = tp / (tp + fp)
print("precision:", precision)
recall = tp / (tp + fn)
print("recall:", recall)
# =======================
precision: 0.6666666666666666
recall: 0.8
F1-score
當 Precision 和Recall 難以取捨、需要考慮到兩者時,可以使用 F1-score, 是Precision 及 Recall的調和平均數。
f1_score = 2*tp / (2*tp + fp + fn)
print("f1_score:", f1_score)
# =======================
f1_score: 0.7272727272727273
F-measure (F-score)
若需要對 Precision、Recall 取不同的權重,則可以將 F1-score 進行擴展,公式如下,當 β²=1 時,就會變為 F1-score。
Sensitivity、Specificity
Sensitivity 表示靈敏度,又稱真陽性率、Recall,定義為所有實際為真的數據中,預測也為真的機率;Specificity 表示特異度,又稱真陰性率,定義為所有實際為假的數據中,預測也為假的機率。
在醫學上經常使用這兩個指標,Sensitivity 指有多少真正得病的人被診斷出有得病,而 Specificity 指有多少沒有得病的人被檢驗正確 (沒得病)。
下圖為 Sensitivity、Specificity 的公式及在 confusion matrix 的關係。
以 Precision、Recall 的例子來計算 Sensitivity 和 Specificity 會得到以下結果。
sensitivity = tp / (tp + fn)
print("sensitivity:", sensitivity)
specificity = tn / (tn + fp)
print("specificity:", specificity)
# =======================
sensitivity: 0.8
specificity: 0.5
P-R Curve
P-R Curve 是以 Recall 為 x 軸、 Precision 為 y 軸所繪製出來的圖,P-R Curve 下的面積 (Area under curve,AUC) 稱為 AP (Average Precision),範圍介於 0 ~ 1 之間。
當 Precision 和 Recall 值越高,P-R Curve 會越往右上方靠近,此時 AUC 越大,表示模型越好。
那我們來計算並畫出 P-R Curve 吧!
- 第一步,整理成表格
一樣以二分類模型為例,下圖為是否有病的 Ground Truth 與 Prediction 結果,先將這些數據整理成表格。
ground_truth = np.array([1, 1, 0, 1, 0, 0, 1, 1, 0])
pred_score = np.array([0.8, 0.4, 0.1, 0.7, 0.6, 0.2, 0.9, 0.8, 0.6])
- 第二步,排序
根據預測結果 (score) 由高至低排序後,令 threshold = 0.5,若 score 大於 threshold 設定為 1,反之則為 0。
threshold = 0.5
zip_list = zip(ground_truth, pred_score)
sort_zip_list = sorted(zip_list, key=lambda x:x[1], reverse=True)
zip_list = zip(*sort_zip_list)
ground_truth, pred_score = [np.array(list(x)) for x in zip_list]
print("ground_truth: ", ground_truth)
print("pred_score: ", pred_score)
pred_label = np.where(pred_score>=threshold, 1, 0)
print("pred_label: ", pred_label)
# =======================
ground_truth: [1 1 1 1 0 0 1 0 0]
pred_score: [0.9 0.8 0.8 0.7 0.6 0.6 0.4 0.2 0.1]
pred_label: [1 1 1 1 1 1 0 0 0]
- 第三步,歸類 TP、FP、FN、TN
再對照 Ground Truth 來分別歸類各個 pixel 為 TP、FP、FN、TN。
def cal_state_list(ground_truth, pred_label):
nd = len(ground_truth)
tp_list = [0] * nd
tn_list = [0] * nd
fn_list = [0] * nd
fp_list = [0] * nd
for i in range(len(ground_truth)):
gt = ground_truth[i]
pred = pred_label[i]
if gt == 1 and pred == 1:
tp_list[i] = 1
elif gt == 1 and pred == 0:
fn_list[i] = 1
elif gt == 0 and pred == 1:
fp_list[i] = 1
else:
tn_list[i] = 1
return tp_list, tn_list, fn_list, fp_list
tp_list, tn_list, fn_list, fp_list = cal_state_list(ground_truth, pred_label)
tp_fn_len = np.sum(fn_list) + np.sum(tp_list)
print("tp_list: ", tp_list)
print("tn_list: ", tn_list)
print("fn_list: ", fn_list)
print("fp_list: ", fp_list)
# =======================
tp_list: [1, 1, 1, 1, 0, 0, 0, 0, 0]
tn_list: [0, 0, 0, 0, 0, 0, 0, 1, 1]
fn_list: [0, 0, 0, 0, 0, 0, 1, 0, 0]
fp_list: [0, 0, 0, 0, 1, 1, 0, 0, 0]
- 第四步,進行計算
然後就可以來開始計算啦~從 rank = 1 開始,代入公式計算 Precision、Recall。要注意的是,分子是累計的,因此 Precision 和 Recall 的分子為皆 1,而不是 4。
計算完 rank 1 後,繼續算 rank = 2。
以此類推,就能夠算出所有 rank 的 Precision 和 Recall。
cumsum = 0
for idx, val in enumerate(fp_list):
fp_list[idx] += cumsum
cumsum += val
cumsum = 0
for idx, val in enumerate(tp_list):
tp_list[idx] += cumsum
cumsum += val
recall = tp_list[:]
for idx, val in enumerate(tp_list):
recall[idx] = float(tp_list[idx]) / tp_fn_len
precision = tp_list[:]
for idx, val in enumerate(tp_list):
precision[idx] = float(tp_list[idx]) / (fp_list[idx] + tp_list[idx])
print("recall: ", recall)
print("precision: ", precision)
# =======================
recall: [0.2, 0.4, 0.6, 0.8, 0.8, 0.8, 0.8, 0.8, 0.8]
precision: [1.0, 1.0, 1.0, 1.0, 0.8, 0.6666666666666666, 0.6666666666666666, 0.6666666666666666, 0.6666666666666666]
- 第五步,畫 P-R Curve
藍色的線是畫圖的結果,P-R Curve 下的面積稱為 AP (Average Precision),即紫色面積的部分。
plt.title('PR Curve', fontsize=15)
plt.xlabel("Recall", fontsize=15)
plt.ylabel("Precision", fontsize=15)
plt.xlim(0, 1.05)
plt.ylim(0.5, 1.05)
plt.xticks(fontsize=12)
plt.yticks(fontsize=12)
plt.scatter(recall, precision)
plt.plot(recall, precision)
plt.show()
ROC Curve
ROC Curve (Receiver Operating Characteristic Curve) 是以 FPR 為 x 軸、 TPR 為 y 軸所繪製出來的圖,其中 FPR (False Positive Rate,偽陽性率) 是指 1- Specificity、TPR (True Positive Rate,真陽性率) 指 Sensitivity。ROC Curve 下的面積 (Area under curve,AUC) 範圍也是介於 0 ~ 1 之間。
當 FPR 越低 (Specificity — 正確檢測出負樣本的機率越高)、TPR 越高,ROC Curve 會越往左上方靠近,此時 AUC 越大,表示模型越好。
接下來,一樣來計算並畫出 ROC Curve 吧!
- 第一步,整理成表格
一樣以二分類模型為例,下圖為是否有病的 Ground Truth 與 Prediction 結果,先將這些數據整理成表格。
ground_truth = np.array([1, 1, 0, 1, 0, 0, 1, 1, 0])
pred_score = np.array([0.8, 0.4, 0.1, 0.7, 0.6, 0.2, 0.9, 0.8, 0.6])
- 第二步,排序
根據預測結果 (score) 由高至低排序。
zip_list = zip(ground_truth, pred_score)
sort_zip_list = sorted(zip_list, key=lambda x:x[1], reverse=True)
zip_list = zip(*sort_zip_list)
ground_truth, pred_score = [np.array(list(x)) for x in zip_list]
print("ground_truth: ", ground_truth)
print("pred_score: ", pred_score)
# =======================
ground_truth: [1 1 1 1 0 0 1 0 0]
pred_score: [0.9 0.8 0.8 0.7 0.6 0.6 0.4 0.2 0.1]
- 第三步,根據 rank 來歸類 TP、FP、FN、TN
這邊跟 P-R Curve 不同,會根據 rank 分別進行計算。設定該 rank 的 score 作為 threshold 以歸類每個 pixel 為 0 或 1,再對照 Ground Truth 求得 TP、FP、FN、TN。
從 rank =1 開始,將 score = 0.9 作為 threshold 來歸類每個 pixel。
- 第四步,進行計算
然後就可以來開始計算啦~代入公式計算 TPR、FPR,分子一樣是使用累計的方式計算。
以此類推,將 rank 1~9 重複進行第三、四步驟,以得到所有 rank 的 TPR 和 FPR。
- 第五步,畫 ROC Curve
藍色的線是畫圖的結果,ROC Curve 下的面積為 AUC (Area under curve),即紫色面積的部分。
plt.title('ROC Curve', fontsize=15)
plt.xlabel("FPR", fontsize=15)
plt.ylabel("TPR", fontsize=15)
plt.xlim(0, 1)
plt.ylim(0, 1.05)
plt.xticks(fontsize=12)
plt.yticks(fontsize=12)
plt.scatter(fpr, tpr)
plt.plot(fpr, tpr)
plt.show()
Dice
在語義分割任務中,經常使用 Dice Loss 作為 loss function,其公式如下:
其中 Dice 係數 (coefficient) 是用於計算 Ground Truth 和 prediction 之間的相似度,|𝑋|、|𝑌| 分別表示為 Ground Truth 和 prediction,其值範圍在 0 ~ 1 之間。
將 Dice 係數公式展開會發現與 F1-score 相同。
接下來試著計算 Dice 係數吧!
假設我們的圖像資料為 9 個 pixel,實際 label 如左圖 (稱為 Ground Truth)、預測 label 如右圖。
用程式碼表示如下
ground_truth = np.array([1, 1, 0, 1, 0, 0, 1, 1, 0])
pred_label = np.array([1, 0, 0, 1, 1, 0, 1, 1, 1])
計算 TP、FP、FN、TN 以及 Dice 係數
### tp
# 所有實際與預測相同的值
gt_equal_pred = np.where(ground_truth==pred_label, 1, 0)
# 實際值與預測值相同,並且實際為真
tp = np.sum(gt_equal_pred * ground_truth)
### fn
# 所有實際為真的數量去除掉 TP
fn = np.sum(ground_truth) - tp
### fp
# 所有預測為真的數量去除掉 TP
fp = np.sum(pred_label) - tp
### tn
# 所有實際為假的值
gt_is_false = np.where(np.logical_not(ground_truth), 1, 0)
# 實際值與預測值相同,並且預測為假
tn = np.sum(gt_equal_pred * gt_is_false)
dice = 2*tp / (2*tp + fn + fp)
print("dice: ", dice)
# =======================
dice: 0.7272727272727273