摘要:ROC/AUC作為機器學習的評估指標非常重要,也是面試中經常出現的問題(80%都會問到)
本文分享自華為雲社區《技術干貨 | 解決面試中80%問題,基於MindSpore實現AUC/ROC》,原文作者:李嘉琪。
ROC/AUC作為機器學習的評估指標非常重要,也是面試中經常出現的問題(80%都會問到)。其實,理解它並不是非常難,但是好多朋友都遇到了一個相同的問題,那就是:每次看書的時候都很明白,但回過頭就忘了,經常容易將概念弄混。還有的朋友面試之前背下來了,但是一緊張大腦一片空白全忘了,導致回答的很差。
我在之前的面試過程中也遇到過類似的問題,我的面試經驗是:一般筆試題遇到選擇題基本都會考這個率,那個率,或者給一個場景讓你選用哪個。面試過程中也被問過很多次,比如什么是AUC/ROC?橫軸縱軸都代表什么?有什么優點?為什么要使用它?
我記得在我第一次回答的時候,我將准確率,精准率,召回率等概念混淆了,最后一團亂。回去以后我從頭到尾梳理了一遍所有相關概念,后面的面試基本都回答地很好。現在想將自己的一些理解分享給大家,希望讀完本篇可以徹底記住ROC/AUC的概念。
ROC的全名叫做Receiver Operating Characteristic,其主要分析工具是一個畫在二維平面上的曲線——ROC 曲線。平面的橫坐標是false positive rate(FPR),縱坐標是true positive rate(TPR)。對某個分類器而言,我們可以根據其在測試樣本上的表現得到一個TPR和FPR點對。這樣,此分類器就可以映射成ROC平面上的一個點。調整這個分類器分類時候使用的閾值,我們就可以得到一個經過(0, 0),(1, 1)的曲線,這就是此分類器的ROC曲線。一般情況下,這個曲線都應該處於(0, 0)和(1, 1)連線的上方。因為(0, 0)和(1, 1)連線形成的ROC曲線實際上代表的是一個隨機分類器。如果很不幸,你得到一個位於此直線下方的分類器的話,一個直觀的補救辦法就是把所有的預測結果反向,即:分類器輸出結果為正類,則最終分類的結果為負類,反之,則為正類。雖然,用ROC 曲線來表示分類器的性能很直觀好用。
可是,人們總是希望能有一個數值來標志分類器的好壞。於是Area Under roc Curve(AUC)就出現了。顧名思義,AUC的值就是處於ROC 曲線下方的那部分面積的大小。通常,AUC的值介於0.5到1.0之間,較大的AUC代表了較好的性能。AUC(Area Under roc Curve)是一種用來度量分類模型好壞的一個標准。
ROC示例曲線(二分類問題):
解讀ROC圖的一些概念定義:
- 真正(True Positive , TP)被模型預測為正的正樣本;
- 假負(False Negative , FN)被模型預測為負的正樣本;
- 假正(False Positive , FP)被模型預測為正的負樣本;
- 真負(True Negative , TN)被模型預測為負的負樣本。
靈敏度,特異度,真正率,假正率
在正式介紹ROC/AUC之前,我們需要介紹兩個指標,這兩個指標的選擇也正是ROC和AUC可以無視樣本不平衡的原因。這兩個指標分別是:靈敏度和(1-特異度),也叫做真正率(TPR)和假正率(FPR)。
靈敏度(Sensitivity) = TP/(TP+FN)
特異度(Specificity) = TN/(FP+TN)
其實我們可以發現靈敏度和召回率是一模一樣的,只是名字換了而已。
由於我們比較關心正樣本,所以需要查看有多少負樣本被錯誤地預測為正樣本,所以使用(1-特異度),而不是特異度。
真正率(TPR) = 靈敏度 = TP/(TP+FN)
假正率(FPR) = 1- 特異度 = FP/(FP+TN)
下面是真正率和假正率的示意,我們發現TPR和FPR分別是基於實際表現1和0出發的,也就是說它們分別在實際的正樣本和負樣本中來觀察相關概率問題。
正因為如此,所以無論樣本是否平衡,都不會被影響。比如總樣本中,90%是正樣本,10%是負樣本。我們知道用准確率是有水分的,但是用TPR和FPR不一樣。這里,TPR只關注90%正樣本中有多少是被真正覆蓋的,而與那10%毫無關系,同理,FPR只關注10%負樣本中有多少是被錯誤覆蓋的,也與那90%毫無關系,所以可以看出:
如果我們從實際表現的各個結果角度出發,就可以避免樣本不平衡的問題了,這也是為什么選用TPR和FPR作為ROC/AUC的指標的原因。
或者我們也可以從另一個角度考慮:條件概率。我們假設X為預測值,Y為真實值。那么就可以將這些指標按條件概率表示:
- 精准率 = P(Y=1 | X=1)
- 召回率 = 靈敏度 = P(X=1 | Y=1)
- 特異度 = P(X=0 | Y=0)
從上面三個公式看到:如果我們先以實際結果為條件(召回率,特異度),那么就只需考慮一種樣本,而先以預測值為條件(精准率),那么我們需要同時考慮正樣本和負樣本。所以先以實際結果為條件的指標都不受樣本不平衡的影響,相反以預測結果為條件的就會受到影響。
ROC(接受者操作特征曲線)
ROC(Receiver Operating Characteristic)曲線,又稱接受者操作特征曲線。該曲線最早應用於雷達信號檢測領域,用於區分信號與噪聲。后來人們將其用於評價模型的預測能力,ROC曲線是基於混淆矩陣得出的。
ROC曲線中的主要兩個指標就是真正率和假正率,上面也解釋了這么選擇的好處所在。其中橫坐標為假正率(FPR),縱坐標為真正率(TPR),下面就是一個標准的ROC曲線圖。
-
ROC曲線的閾值問題
與前面的P-R曲線類似,ROC曲線也是通過遍歷所有閾值來繪制整條曲線的。如果我們不斷的遍歷所有閾值,預測的正樣本和負樣本是在不斷變化的,相應的在ROC曲線圖中也會沿着曲線滑動。
-
如何判斷ROC曲線的好壞?
改變閾值只是不斷地改變預測的正負樣本數,即TPR和FPR,但是曲線本身是不會變的。那么如何判斷一個模型的ROC曲線是好的呢?這個還是要回歸到我們的目的:FPR表示模型虛報的響應程度,而TPR表示模型預測響應的覆蓋程度。我們所希望的當然是:虛報的越少越好,覆蓋的越多越好。所以總結一下就是TPR越高,同時FPR越低(即ROC曲線越陡),那么模型的性能就越好。參考如下動態圖進行理解。
-
ROC曲線無視樣本不平衡
前面已經對ROC曲線為什么可以無視樣本不平衡做了解釋,下面我們用動態圖的形式再次展示一下它是如何工作的。我們發現:無論紅藍色樣本比例如何改變,ROC曲線都沒有影響。
AUC(曲線下的面積)
為了計算 ROC 曲線上的點,我們可以使用不同的分類閾值多次評估邏輯回歸模型,但這樣做效率非常低。幸運的是,有一種基於排序的高效算法可以為我們提供此類信息,這種算法稱為曲線下面積(Area Under Curve)。
比較有意思的是,如果我們連接對角線,它的面積正好是0.5。對角線的實際含義是:隨機判斷響應與不響應,正負樣本覆蓋率應該都是50%,表示隨機效果。ROC曲線越陡越好,所以理想值就是1,一個正方形,而最差的隨機判斷都有0.5,所以一般AUC的值是介於0.5到1之間的。
-
AUC的一般判斷標准
0.5 - 0.7:效果較低,但用於預測股票已經很不錯了0.7 - 0.85:效果一般0.85 - 0.95:效果很好0.95 - 1:效果非常好,但一般不太可能
-
AUC的物理意義
曲線下面積對所有可能的分類閾值的效果進行綜合衡量。曲線下面積的一種解讀方式是看作模型將某個隨機正類別樣本排列在某個隨機負類別樣本之上的概率。以下面的樣本為例,邏輯回歸預測從左到右以升序排列:
好了,原理已經講完,上MindSpore框架的代碼。
MindSpore代碼實現(ROC)
"""ROC""" import numpy as np from mindspore._checkparam import Validator as validator from .metric import Metric class ROC(Metric): def __init__(self, class_num=None, pos_label=None): super().__init__() # 分類數為一個整數 self.class_num = class_num if class_num is None else validator.check_value_type("class_num", class_num, [int]) # 確定正類的整數,對於二分類問題,它被轉換為1。對於多分類問題,不應設置此參數,因為它在[0,num_classes-1]范圍內迭代更改。 self.pos_label = pos_label if pos_label is None else validator.check_value_type("pos_label", pos_label, [int]) self.clear() def clear(self): """清除歷史數據""" self.y_pred = 0 self.y = 0 self.sample_weights = None self._is_update = False def _precision_recall_curve_update(self, y_pred, y, class_num, pos_label): """更新曲線""" if not (len(y_pred.shape) == len(y.shape) or len(y_pred.shape) == len(y.shape) + 1): raise ValueError("y_pred and y must have the same number of dimensions, or one additional dimension for" " y_pred.") # 二分類驗證 if len(y_pred.shape) == len(y.shape): if class_num is not None and class_num != 1: raise ValueError('y_pred and y should have the same shape, but number of classes is different from 1.') class_num = 1 if pos_label is None: pos_label = 1 y_pred = y_pred.flatten() y = y.flatten() # 多分類驗證 elif len(y_pred.shape) == len(y.shape) + 1: if pos_label is not None: raise ValueError('Argument `pos_label` should be `None` when running multiclass precision recall ' 'curve, but got {}.'.format(pos_label)) if class_num != y_pred.shape[1]: raise ValueError('Argument `class_num` was set to {}, but detected {} number of classes from ' 'predictions.'.format(class_num, y_pred.shape[1])) y_pred = y_pred.transpose(0, 1).reshape(class_num, -1).transpose(0, 1) y = y.flatten() return y_pred, y, class_num, pos_label def update(self, *inputs): """ 更新預測值和真實值。 """ # 輸入數量的校驗 if len(inputs) != 2: raise ValueError('ROC need 2 inputs (y_pred, y), but got {}'.format(len(inputs))) # 將輸入轉為numpy y_pred = self._convert_data(inputs[0]) y = self._convert_data(inputs[1]) # 更新曲線 y_pred, y, class_num, pos_label = self._precision_recall_curve_update(y_pred, y, self.class_num, self.pos_label) self.y_pred = y_pred self.y = y self.class_num = class_num self.pos_label = pos_label self._is_update = True def _roc_(self, y_pred, y, class_num, pos_label, sample_weights=None): if class_num == 1: fps, tps, thresholds = self._binary_clf_curve(y_pred, y, sample_weights=sample_weights, pos_label=pos_label) tps = np.squeeze(np.hstack([np.zeros(1, dtype=tps.dtype), tps])) fps = np.squeeze(np.hstack([np.zeros(1, dtype=fps.dtype), fps])) thresholds = np.hstack([thresholds[0][None] + 1, thresholds]) if fps[-1] <= 0: raise ValueError("No negative samples in y, false positive value should be meaningless.") fpr = fps / fps[-1] if tps[-1] <= 0: raise ValueError("No positive samples in y, true positive value should be meaningless.") tpr = tps / tps[-1] return fpr, tpr, thresholds # 定義三個列表 fpr, tpr, thresholds = [], [], [] for c in range(class_num): preds_c = y_pred[:, c] res = self.roc(preds_c, y, class_num=1, pos_label=c, sample_weights=sample_weights) fpr.append(res[0]) tpr.append(res[1]) thresholds.append(res[2]) return fpr, tpr, thresholds def roc(self, y_pred, y, class_num=None, pos_label=None, sample_weights=None): """roc""" y_pred, y, class_num, pos_label = self._precision_recall_curve_update(y_pred, y, class_num, pos_label) return self._roc_(y_pred, y, class_num, pos_label, sample_weights) def (self): """ 計算ROC曲線。返回的是一個元組,由`fpr`、 `tpr`和 `thresholds`組成的元組。 """ if self._is_update is False: raise RuntimeError('Call the update method before calling .') y_pred = np.squeeze(np.vstack(self.y_pred)) y = np.squeeze(np.vstack(self.y)) return self._roc_(y_pred, y, self.class_num, self.pos_label)
使用方法如下:
- 二分類的例子
import numpy as np from mindspore import Tensor from mindspore.nn.metrics import ROC # binary classification example x = Tensor(np.array([3, 1, 4, 2])) y = Tensor(np.array([0, 1, 2, 3])) metric = ROC(pos_label=2) metric.clear() metric.update(x, y) fpr, tpr, thresholds = metric.() print(fpr, tpr, thresholds) [0., 0., 0.33333333, 0.6666667, 1.] [0., 1, 1., 1., 1.] [5, 4, 3, 2, 1]
- 多分類的例子
import numpy as np from mindspore import Tensor from mindspore.nn.metrics import ROC # multiclass classification example x = Tensor(np.array([[0.28, 0.55, 0.15, 0.05], [0.10, 0.20, 0.05, 0.05], [0.20, 0.05, 0.15, 0.05],0.05, 0.05, 0.05, 0.75]])) y = Tensor(np.array([0, 1, 2, 3])) metric = ROC(class_num=4) metric.clear() metric.update(x, y) fpr, tpr, thresholds = metric.() print(fpr, tpr, thresholds) [array([0., 0., 0.33333333, 0.66666667, 1.]), array([0., 0.33333333, 0.33333333, 1.]), array([0., 0.33333333, 1.]), array([0., 0., 1.])] [array([0., 1., 1., 1., 1.]), array([0., 0., 1., 1.]), array([0., 1., 1.]), array([0., 1., 1.])] [array([1.28, 0.28, 0.2, 0.1, 0.05]), array([1.55, 0.55, 0.2, 0.05]), array([1.15, 0.15, 0.05]), array([1.75, 0.75, 0.05])]
MindSpore代碼實現(AUC)
"""auc""" import numpy as np def auc(x, y, reorder=False): """ 使用梯形法則計算曲線下面積(AUC)。這是一個一般函數,給定曲線上的點。計算ROC曲線下的面積。 """ # 輸入x是由ROC曲線得到的fpr值或者一個假陽性numpy數組。如果是多類的,這是一個這樣的list numpy,每組代表一類。 # 輸入y是由ROC曲線得到的tpr值或者一個真陽性numpy數組。如果是多類的,這是一個這樣的list numpy,每組代表一類。 if not isinstance(x, np.ndarray) or not isinstance(y, np.ndarray): raise TypeError('The inputs must be np.ndarray, but got {}, {}'.format(type(x), type(y))) # 檢查所有數組的第一個維度是否一致。檢查數組中的所有對象是否具有相同的形狀或長度。 _check_consistent_length(x, y) # 展開列或1d numpy數組。 x = _column_or_1d(x) y = _column_or_1d(y) # 進行校驗 if x.shape[0] < 2: raise ValueError('At least 2 points are needed to compute the AUC, but x.shape = {}.'.format(x.shape)) direction = 1 if reorder: order = np.lexsort((y, x)) x, y = x[order], y[order] else: dx = np.diff(x) if np.any(dx < 0): if np.all(dx 1: raise ValueError("Found input variables with inconsistent numbers of samples: {}." .format([int(length) for length in lengths]))
使用方法如下:
- 利用ROC的fpr, tpr值求auc
import numpy as np from mindspore.nn.metrics import auc x = Tensor(np.array([[3, 0, 1], [1, 3, 0], [1, 0, 2]])) y = Tensor(np.array([[0, 2, 1], [1, 2, 1], [0, 0, 1]])) metric = ROC(pos_label=1) metric.clear() metric.update(x, y) fpr, tpr, thre = metric.eval() # 利用ROC的fpr, tpr值求auc output = auc(fpr, tpr) print(output) 0.45