終於搞懂了PR曲線
PR(Precision Recall)曲線
問題
最近項目中遇到一個比較有意思的問題, 如下所示為:
圖中的PR
曲線很奇怪, 左邊從1突然變到0.
PR源碼分析
為了搞清楚這個問題, 對源碼進行了分析. 如下所示為上圖對應的代碼:
from sklearn.metrics import precision_recall_curve
import matplotlib.pyplot as plt
score = np.array([0.9, 0.8, 0.7, 0.6, 0.3, 0.2, 0.1])
label = np.array([0, 1, 1, 1, 0, 0, 0])
precision, recall, thres = precision_recall_curve(label, score)
plt.plot(recall, precision)
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.show()
代碼中得到precision
和recall
使用的是sklearn.metrics.precision_recall_curve
, 下面為從其對應的源碼中抽取出來的關鍵代碼:
# 按預測概率(score)降序排列
desc_score_indices = np.argsort(y_score, kind="mergesort")[::-1]
y_score = y_score[desc_score_indices]
y_true = y_true[desc_score_indices]
# 概率(score)閾值, 取所有概率中不相同的
distinct_value_indices = np.where(np.diff(y_score))[0]
threshold_idxs = np.r_[distinct_value_indices, y_true.size-1]
thresholds = y_score[threshold_idxs]
# 累計求和, 得到不同閾值下的 tps, fps
tps = np.cumsum(y_true)[threshold_idxs]
fps = 1 + threshold_idxs - tps
# PR
precision = tps / (tps + fps)
precision[np.isnan(precision)] = 0 # 將nan替換為0
recall = tps / tps[-1]
last_ind = tps.searchsorted(tps[-1]) # 最後一個tps的index
sl = slice(last_ind, None, -1) # 倒序
precision = np.r_[precision[sl], 1] # 添加 precision=1, recall=0, 可以讓數據從0開始
recall = np.r_[recall[sl], 0]
從代碼中總結了計算PR
的幾個關鍵步驟:
- 對於預測概率(score)排序, 從高到低
- 以預測概率(score)作為閾值統計
tps
和fps
- 計算
precision
和recall
, 並倒序
這裡補充說明幾個特點:
- 以測試數據的預測概率(score)作為閾值, 因而閾值只能在測試數據預測概率(score)集合中, 不是連續變化的;
- 統計
tps
和fps
時, 統計的是大於等於閾值的數據的個數, 因而理想情況下,tps>=1
和fps>=1
, 這裡說的是理想情況下, 不理想情況後面說明; - 測試數據預測概率(score)可能不會出現為1的情況, 此種情況下,
recall=0
, 為了使得PR
曲線從0開始, 添加了recall=0, precision=1
; - 使用倒序, 讓閾值從小到大, 因而
PR
曲線是從左向右畫的, 如下圖所示:
問題原因分析
弄清楚了PR
原理及計算方法, 就好分析上述問題產生的原因了.
1的來歷
從上述原理及計算過程分析可以看到, 最後添加了recall=0, precision=1
, 對應圖中最左邊的1, 這裡就知道了1是怎麼來的;
0的來歷
precision
的計算公式是precision=TP/(TP+FP)
, 理想情況下(score值越大, Positive的可能性就越大), 隨着閾值的增加, TP越來越小, FP越來越小, precision是越來越大的, 是不可能出現為0情況的; 只有當TP=0時, precision才會出現為0的情況, 這種情況屬於非理想情況(score值越大, Positive的可能性不一定越大).
來看看tps
的計算方法, 統計的是大於等於閾值thres的數據中為Positive的個數, 只有Positive個數為0的情況下, tps才能為0, 那麼thres對應的數據就不是Positive的, 而是Negative的.
我們來看看上面例子中的數據:
score | 0.9 | 0.8 | 0.7 | 0.6 | 0.3 | 0.2 | 0.1 |
---|---|---|---|---|---|---|---|
label | 0 | 1 | 1 | 1 | 0 | 0 | 0 |
從上表中可以看到, 最大score=0.9的標籤為0, 這裡對應圖中precision=0
的情況, 這裡就知道了0是怎麼來的: 數據中有存在最高概率為Negative的數據.
這裡可以做個擴展, 理想情況下, PR
曲線從右向左, precision
應該是越來越大的, 如果出現了減小或者變為0的情況, 可看看對應閾值下的數據是否存在標籤有誤, 或者是困難樣本.
解決方法
最好的方法, 是通過PR曲線分析是否存在標籤有錯誤的樣本或者困難樣本, 然後對測試樣本進行調整.
這裡有2個折中的解決方法, 可以去除這種突變:
一是限制顯示範圍:
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
一是把最後一個數據去除:
precision = precision[:-1]
recall = recall[:-1]
PR 與 ROC(Receiver Operating Characteristics)曲線
相互關係
有文章已經證明, PR 和 ROC 可以相互轉換:
Theorem 3.1. For a given dataset of positive and negative examples, there exists a one-to-one correspon- dence between a curve in ROCspace and a curve in PR space, such that the curves contain exactly the same confusion matrices, if Recall != 0
詳見: The Relationship Between Precision-Recall and ROC Curves, 網上也有很多資料有詳細的說明, 下圖為二者的變化趨勢:
優劣
PR 和 ROC 的區別主要在於不平衡數據的表現: PR對數據不平衡是敏感的, 正負樣本比例變化會引起PR發生很大的變化; 而ROC曲線是不敏感的, 正負樣本比例變化時ROC曲線變化很小. 如下圖所示為不同比例正負樣本情況下PR和ROC的變化:
ROC曲線變化很小的原因分析: tpr=TP/P, fpr=FP/N, 可以看到其計算都是在類別內部進行計算的, 只要數據內部的比例不發生變化, ROC也不會發生變化.
參考: