摘要:ROC/AUC做爲機器學習的評估指標很是重要,也是面試中常常出現的問題(80%都會問到)
本文分享自華爲雲社區《技術乾貨 | 解決面試中80%問題,基於MindSpore實現AUC/ROC》,原文做者:李嘉琪。html
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 曲線來表示分類器的性能很直觀好用。app
但是,人們老是但願能有一個數值來標誌分類器的好壞。因而Area Under roc Curve(AUC)就出現了。顧名思義,AUC的值就是處於ROC 曲線下方的那部分面積的大小。一般,AUC的值介於0.5到1.0之間,較大的AUC表明了較好的性能。AUC(Area Under roc Curve)是一種用來度量分類模型好壞的一個標準。框架
ROC示例曲線(二分類問題):less
解讀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