­

終於搞懂了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()

代碼中得到precisionrecall使用的是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的幾個關鍵步驟:

  1. 對於預測概率(score)排序, 從高到低
  2. 以預測概率(score)作為閾值統計tpsfps
  3. 計算precisionrecall, 並倒序

這裡補充說明幾個特點:

  1. 以測試數據的預測概率(score)作為閾值, 因而閾值只能在測試數據預測概率(score)集合中, 不是連續變化的;
  2. 統計tpsfps時, 統計的是大於等於閾值的數據的個數, 因而理想情況下, tps>=1fps>=1, 這裡說的是理想情況下, 不理想情況後面說明;
  3. 測試數據預測概率(score)可能不會出現為1的情況, 此種情況下, recall=0, 為了使得PR曲線從0開始, 添加了recall=0, precision=1;
  4. 使用倒序, 讓閾值從小到大, 因而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也不會發生變化.

參考:

  1. 分類模型評估之ROC-AUC曲線和PRC曲線_皮皮blog-CSDN博客_auc曲線

  2. 精確率、召回率、F1 值、ROC、AUC 各自的優缺點是什麼? – 知乎 (zhihu.com)