How to measure the quality of classification results?
在剛接觸機(jī)器學(xué)習(xí)的時候,我們可以自己動手完成一個簡單的KNN算法,但是我們不禁會疑惑,它的效果怎樣?在機(jī)器學(xué)習(xí)中如何評價一個算法的好壞?我們在機(jī)器學(xué)習(xí)過程中還有需要注意那些其他的問題呢?
我們?nèi)匀灰砸粋€著名的鳶尾花數(shù)據(jù)集來學(xué)習(xí)機(jī)器學(xué)習(xí)中判斷模型的有些的各種指標(biāo)。
一、數(shù)據(jù)準(zhǔn)備
對于算法優(yōu)劣的度量,通常的做法是將原始數(shù)據(jù)中的一部分作為訓(xùn)練數(shù)據(jù)、另一部分作為測試數(shù)據(jù)。使用訓(xùn)練數(shù)據(jù)訓(xùn)練模型,再用測試數(shù)據(jù)對模型進(jìn)行驗(yàn)證。
1.1、導(dǎo)入鳶尾花數(shù)據(jù)集
import numpy as np
from sklearn import datasets
import matplotlib.pyplot as plt
import pandas as pd
iris = datasets.load_iris()
X = iris.data
y = iris.target
X.shape
y.shape
(150,)
可以看出數(shù)據(jù)集保存在X中,分類的結(jié)果保存在了y中
1.2、拆分?jǐn)?shù)據(jù)
一般情況下我們按照0.8:0.2的比例進(jìn)行拆分,但是有時候我們不能簡單地把前n個數(shù)據(jù)作為訓(xùn)練數(shù)據(jù)集,后n個作為測試數(shù)據(jù)集,比如鳶尾花數(shù)據(jù)集中的數(shù)據(jù)是有序排列的,如下:
y
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2])
從以上的數(shù)據(jù)可知,要想使拆分?jǐn)?shù)據(jù)集變得有意義,為了解決這個問題,我們可以將數(shù)據(jù)集打亂,做一個shuffle操作。但是本數(shù)據(jù)集的特征和標(biāo)簽是分開的,也就是說我們分別亂序后,原來的對應(yīng)關(guān)系就不存在了。有兩種方法解決這一問題:
1.將X和y合并為同一個矩陣,然后對矩陣進(jìn)行shuffle,之后再分解
2.對y的索引進(jìn)行亂序,根據(jù)索引確定與X的對應(yīng)關(guān)系,最后再通過亂序的索引進(jìn)行賦值
為了鞏固基礎(chǔ),下面我們自己動手來完成數(shù)據(jù)集的拆分
第一種方式
# 使用concatenate函數(shù)進(jìn)行拼接,因?yàn)閭魅氲木仃嚤仨毦哂邢嗤男螤睢?# 因此需要對label進(jìn)行reshape操作,reshape(-1,1)表示行數(shù)自動計(jì)算,1列。
# axis=1表示縱向拼接。
tempConcat = np.concatenate((X, y.reshape(-1,1)), axis=1)
# 拼接好后,直接進(jìn)行亂序操作
np.random.shuffle(tempConcat)
# 再將shuffle后的數(shù)組使用split方法拆分
shuffle_X,shuffle_y = np.split(tempConcat, [4], axis=1)
# 設(shè)置劃分的比例
test_ratio = 0.2
test_size = int(len(X) * test_ratio)
X_train = shuffle_X[test_size:]
y_train = shuffle_y[test_size:]
X_test = shuffle_X[:test_size]
y_test = shuffle_y[:test_size]
第二種方式
# 將x長度這么多的數(shù),返回一個新的打亂順序的數(shù)組,注意,數(shù)組中的元素不是原來的數(shù)據(jù),而是混亂的索引
shuffle_index = np.random.permutation(len(X))
# 指定測試數(shù)據(jù)的比例
test_ratio = 0.2
test_size = int(len(X) * test_ratio)
test_index = shuffle_index[:test_size]
train_index = shuffle_index[test_size:]
X_train = X[train_index]
X_test = X[test_index]
y_train = y[train_index]
y_test = y[test_index]
將拆分方法封裝成與sklearn中同名的函數(shù)
import numpy as np
def train_test_split_temp(X, y, test_ratio=0.2, seed=None):
"""將矩陣X和標(biāo)簽y按照test_ration分割成X_train, X_test, y_train, y_test"""
assert X.shape[0] == y.shape[0], "the size of X must be equal to the size of y"
assert 0.0 <= test_ratio <= 1.0, "test_train must be valid"
if seed: # 是否使用隨機(jī)種子,使隨機(jī)結(jié)果相同,方便debug
np.random.seed(seed) # permutation(n) 可直接生成一個隨機(jī)排列的數(shù)組,含有n個元素
shuffle_index = np.random.permutation(len(X))
test_size = int(len(X) * test_ratio)
test_index = shuffle_index[:test_size]
train_index = shuffle_index[test_size:]
X_train = X[train_index]
X_test = X[test_index]
y_train = y[train_index]
y_test = y[test_index]
return X_train, X_test, y_train, y_test
X_train, X_test, y_train, y_test = train_test_split_temp(X, y)
sklearn中的train_test_split
我們自己寫的train_test_split其實(shí)也是在模仿sklearn風(fēng)格,更多的時候我們可以直接調(diào)用。
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=666)
print(X_train.shape)
print(X_test.shape)
print(y_train.shape)
print(y_test.shape)
(120, 4)
(30, 4)
(120,)
(30,)
from sklearn.neighbors import KNeighborsClassifier
# 創(chuàng)建kNN_classifier實(shí)例
kNN_classifier = KNeighborsClassifier(n_neighbors=3)
# kNN_classifier做一遍fit(擬合)的過程,沒有返回值,
# 模型就存儲在kNN_classifier實(shí)例中
kNN_classifier.fit(X_train, y_train)
# kNN進(jìn)行預(yù)測predict,需要傳入一個矩陣,而不能是一個數(shù)組。reshape()成一個二維數(shù)組,第一個參數(shù)是1表示只有一個數(shù)據(jù),第二個參數(shù)-1,numpy自動決定第二維度有多少
y_predict = kNN_classifier.predict(X_test)
y_predict
array([1, 2, 1, 2, 0, 1, 1, 2, 1, 1, 1, 0, 0, 0, 2, 1, 0, 2, 2, 2, 1, 0,
2, 0, 1, 1, 0, 1, 2, 2])
二、分類準(zhǔn)確度
在劃分出測試數(shù)據(jù)集后,我們就可以驗(yàn)證其模型準(zhǔn)確率了。在這了引出一個非常簡單且常用的概念:accuracy(分類準(zhǔn)確度)
accuracy_score:函數(shù)計(jì)算分類準(zhǔn)確率,返回被正確分類的樣本比例(default)或者是數(shù)量(normalize=False)
在多標(biāo)簽分類問題中,該函數(shù)返回子集的準(zhǔn)確率,對于一個給定的多標(biāo)簽樣本,如果預(yù)測得到的標(biāo)簽集合與該樣本真正的標(biāo)簽集合嚴(yán)格吻合,則subset accuracy =1.0否則是0.0
因accuracy定義清洗、計(jì)算方法簡單,因此經(jīng)常被使用。但是它在某些情況下并不一定是評估模型的最佳工具。精度(查準(zhǔn)率)和召回率(查全率)等指標(biāo)對衡量機(jī)器學(xué)習(xí)的模型性能在某些場合下要比accuracy更好。
當(dāng)然這些指標(biāo)在后續(xù)都會介紹。在這里我們就使用分類精準(zhǔn)度,并將其作用于一個新的手寫數(shù)字識別分類算法上。
對于上面的鳶尾花分類,準(zhǔn)確度=分類正確的數(shù)量/全部測試樣本的數(shù)量
print(sum(y_test == y_predict)/len(y_test))
1.0
再通過一個例子來鞏固一下
2.1、數(shù)據(jù)探索
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
# 手寫數(shù)字?jǐn)?shù)據(jù)集,封裝好的對象,可以理解為一個字段
digits = datasets.load_digits()
# 可以使用keys()方法來看一下數(shù)據(jù)集的詳情
digits.keys()
dict_keys(['data', 'target', 'target_names', 'images', 'DESCR'])
我們可以看一下sklearn.datasets提供的數(shù)據(jù)描述:
5620張圖片,每張圖片有64個像素點(diǎn)即特征(8*8整數(shù)像素圖像),每個特征的取值范圍是1~16(sklearn中的不全),對應(yīng)的分類結(jié)果是10個數(shù)字print(digits.DESCR)
下面我們根據(jù)datasets提供的方法,進(jìn)行簡單的數(shù)據(jù)探索。
# 特征的shape
X = digits.data
X.shape
(1797, 64)
# 標(biāo)簽的shape
y = digits.target
y.shape
(1797,)
# # 標(biāo)簽分類
digits.target_names
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
# # 去除某一個具體的數(shù)據(jù),查看其特征以及標(biāo)簽信息
some_digit = X[0]
some_digit
array([ 0., 0., 5., 13., 9., 1., 0., 0., 0., 0., 13., 15., 10.,
15., 5., 0., 0., 3., 15., 2., 0., 11., 8., 0., 0., 4.,
12., 0., 0., 8., 8., 0., 0., 5., 8., 0., 0., 9., 8.,
0., 0., 4., 11., 0., 1., 12., 7., 0., 0., 2., 14., 5.,
10., 12., 0., 0., 0., 0., 6., 13., 10., 0., 0., 0.])
# # 也可以這條數(shù)據(jù)進(jìn)行可視化
some_digmit_image = some_digit.reshape(8, 8)
plt.imshow(some_digmit_image, cmap = matplotlib.cm.binary)
plt.show()

2.2、自己實(shí)現(xiàn)分類準(zhǔn)確度
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=666)
print(X_train.shape)
print(X_test.shape)
print(y_train.shape)
print(y_test.shape)
(1437, 64)
(360, 64)
(1437,)
(360,)
np.ceil(np.log2(X.shape[0]))
11.0
knn = KNeighborsClassifier(n_neighbors=11)
knn.fit(X_train, y_train)
y_predict = knn.predict(X_test)
print(sum(y_test == y_predict)/len(y_test))
0.9861111111111112
2.3、sklearn中的準(zhǔn)確度
from sklearn.metrics import accuracy_score
accuracy_score(y_test, y_predict)
0.9861111111111112
三、混淆矩陣
討論混淆矩陣之前,我們先思考這樣一個問題:
對于一個癌癥預(yù)測系統(tǒng),輸入檢查指標(biāo),判斷是否患有癌癥,預(yù)測準(zhǔn)確度99.9%。這個系統(tǒng)是好是壞呢?
如果癌癥產(chǎn)生的概率是0.1%,那其實(shí)根本不需要任何機(jī)器學(xué)習(xí)算法,只要系統(tǒng)預(yù)測所有人都是健康的,即可達(dá)到99.9%的準(zhǔn)確率。也就是說對于極度偏斜(Skewed Data)的數(shù)據(jù),只使用分類準(zhǔn)確度是不能衡量。
這是就需要使用混淆矩陣(Confusion Matrix)做進(jìn)一步分析。
3.1、什么是混淆矩陣?
對于二分類問題來說,所有的問題被分為0和1兩類,混淆矩陣是2*2的矩陣:
| 預(yù)測值0 | 預(yù)測值1 | |
|---|---|---|
| 真實(shí)值0 | TN | FP |
| 真實(shí)值1 | FN | TP |
-
TN:真實(shí)值是0,預(yù)測值也是0,即我們預(yù)測是negative,預(yù)測正確了。 -
FP:真實(shí)值是0,預(yù)測值是1,即我們預(yù)測是positive,但是預(yù)測錯誤了。 -
FN:真實(shí)值是1,預(yù)測值是0,即我們預(yù)測是negative,但預(yù)測錯誤了。 -
TP:真實(shí)值是1,預(yù)測值是1,即我們預(yù)測是positive,預(yù)測正確了。
現(xiàn)在假設(shè)有1萬人進(jìn)行預(yù)測,填入混淆矩陣如下:
| 預(yù)測值0 | 預(yù)測值1 | |
|---|---|---|
| 真實(shí)值0 | 9978 | 12 |
| 真實(shí)值1 | 2 | 8 |
對于1萬個人中,有9978個人本身并沒有癌癥,我們的算法也判斷他沒有癌癥;有12個人本身沒有癌癥,但是我們的算法卻錯誤地預(yù)測他有癌癥;有2個人確實(shí)有癌癥,但我們算法預(yù)測他沒有癌癥;有8個人確實(shí)有癌癥,而且我們也預(yù)測對了。
因?yàn)榛煜仃嚤磉_(dá)的信息比簡單的分類準(zhǔn)確度更全面,因此可以通過混淆矩陣得到一些有效的指標(biāo)。
3.2、混淆矩陣的代碼實(shí)現(xiàn)
實(shí)現(xiàn)一個邏輯回歸算法
import numpy as np
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
digits = datasets.load_digits()
X = digits.data
y = digits.target.copy()
# 要構(gòu)造偏斜數(shù)據(jù),將數(shù)字9的對應(yīng)索引的元素設(shè)置為1,0~8設(shè)置為0
y[digits.target==9]=1
y[digits.target!=9]=0
# 使用邏輯回歸做一個分類
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=666)
log_reg = LogisticRegression()
log_reg.fit(X_train,y_train)
# 得到X_test所對應(yīng)的預(yù)測值
y_log_predict = log_reg.predict(X_test)
log_reg.score(X_test, y_test)
0.9755555555555555
定義混淆矩陣的四個指標(biāo):TN
def TN(y_true, y_predict):
assert len(y_true) == len(y_predict)
# (y_true == 0):向量與數(shù)值按位比較,得到的是一個布爾向量
# 向量與向量按位與,結(jié)果還是布爾向量
# np.sum 計(jì)算布爾向量中True的個數(shù)(True記為1,F(xiàn)alse記為0)
return np.sum((y_true == 0) & (y_predict == 0))
# 向量與向量按位與,結(jié)果還是向量
TN(y_test, y_log_predict)
403
定義混淆矩陣的四個指標(biāo):FP
def FP(y_true, y_predict):
assert len(y_true) == len(y_predict)
# (y_true == 0):向量與數(shù)值按位比較,得到的是一個布爾向量
# 向量與向量按位與,結(jié)果還是布爾向量
# np.sum 計(jì)算布爾向量中True的個數(shù)(True記為1,F(xiàn)alse記為0)
return np.sum((y_true == 0) & (y_predict == 1)) # 向量與向量按位與,結(jié)果還是向量
FP(y_test, y_log_predict)
2
定義混淆矩陣的四個指標(biāo):FN
def FN(y_true, y_predict):
assert len(y_true) == len(y_predict)
# (y_true == 0):向量與數(shù)值按位比較,得到的是一個布爾向量
# 向量與向量按位與,結(jié)果還是布爾向量
# np.sum 計(jì)算布爾向量中True的個數(shù)(True記為1,F(xiàn)alse記為0)
return np.sum((y_true == 1) & (y_predict == 0)) # 向量與向量按位與,結(jié)果還是向量
FN(y_test, y_log_predict)
9
定義混淆矩陣的四個指標(biāo):TP
def TP(y_true, y_predict):
assert len(y_true) == len(y_predict)
# (y_true == 0):向量與數(shù)值按位比較,得到的是一個布爾向量
# 向量與向量按位與,結(jié)果還是布爾向量
# np.sum 計(jì)算布爾向量中True的個數(shù)(True記為1,F(xiàn)alse記為0)
return np.sum((y_true == 1) & (y_predict == 1)) # 向量與向量按位與,結(jié)果還是向量
TP(y_test, y_log_predict)
36
輸出混淆矩陣
import pandas as pd
def confusion_matrix(y_true, y_predict):
return pd.DataFrame(np.array([
[TN(y_true, y_predict), FP(y_true, y_predict)],
[FN(y_true, y_predict), TP(y_true, y_predict)]])
,index=['實(shí)際值0', '實(shí)際值1']
,columns=["預(yù)測值0", '預(yù)測值1'])
confusion_matrix(y_test, y_log_predict)
# pd.DataFrame(confusion_matrix(y_test, y_log_predict)
# ,index=['實(shí)際值0', '實(shí)際值1']
# ,columns=["預(yù)測值0", '預(yù)測值1'])

3.3、scikit-learn中的混淆矩陣
from sklearn.metrics import confusion_matrix
# cm = confusion_matrix(y_test, y_log_predict)
cm = pd.crosstab(y_log_predict,y_test)
# cm
# 導(dǎo)入第三方模塊
import seaborn as sns
# 將混淆矩陣構(gòu)造成數(shù)據(jù)框,并加上字段名和行名稱,用于行或列的含義說明
cm = pd.DataFrame(cm)
# 繪制熱力圖
sns.heatmap(cm, annot = True,cmap = 'GnBu', fmt='g')
# 添加x軸和y軸的標(biāo)簽
plt.xlabel(' Real Lable')
plt.ylabel(' Predict Lable')
# 圖形顯示
Text(33,0.5,' Predict Lable')

四、精準(zhǔn)率與召回率
精準(zhǔn)率:
即精準(zhǔn)率為8/(8+12)=40%。所謂的精準(zhǔn)率是:分母為所有預(yù)測為1的個數(shù),分子是其中預(yù)測對了的個數(shù),即預(yù)測值為1,且預(yù)測對了的比例。
為什么管它叫精準(zhǔn)率呢?在有偏的數(shù)據(jù)中,我們通常更關(guān)注值為1的特征,比如“患病”,比如“有風(fēng)險”。在100次結(jié)果為患病的預(yù)測,平均有40次預(yù)測是對的。即精準(zhǔn)率為我們關(guān)注的那個事件,預(yù)測的有多準(zhǔn)。
召回率:
即精準(zhǔn)率為8/(8+2)=80%。所謂召回率是:所有真實(shí)值為1的數(shù)據(jù)中,預(yù)測對了的個數(shù)。每當(dāng)有100個癌癥患者,算法可以成功的預(yù)測出8個 。也就是我們關(guān)注的那個事件真實(shí)的發(fā)生情況下,我們成功預(yù)測的比例是多少。
那么為什么需要精準(zhǔn)率和召回率呢?還是下面的這個例子,有10000個人,混淆矩陣如下:
| 預(yù)測值0 | 預(yù)測值1 | |
|---|---|---|
| 真實(shí)值0 | 9978 | 12 |
| 真實(shí)值1 | 2 | 8 |
如果我們粗暴的認(rèn)為所有人都是健康的,那算法的準(zhǔn)確率是99.78%,但這是毫無意義的。如果算精準(zhǔn)率則是40%,召回率是80%。
4.1、精準(zhǔn)率的代碼實(shí)現(xiàn)
def precision_score(y_true, y_predict):
tp = TP(y_true, y_predict)
fp = FP(y_true, y_predict)
try:
return tp / (tp + fp)
except:
return 0.0
precision_score(y_test, y_log_predict)
0.9473684210526315
4.2、召回率的代碼實(shí)現(xiàn)
def recall_score(y_true, y_predict):
tp = TP(y_true, y_predict)
fn = FN(y_true, y_predict)
try:
return tp / (tp + fn)
except:
return 0.0
recall_score(y_test, y_log_predict)
0.8
4.3、scikit-learn中的精準(zhǔn)率與召回率
from sklearn.metrics import precision_score
precision_score(y_test, y_log_predict)
0.9473684210526315
from sklearn.metrics import recall_score
recall_score(y_test, y_log_predict)
0.8
4.4、精準(zhǔn)率與召回率的關(guān)系與側(cè)重點(diǎn)
精準(zhǔn)率(查準(zhǔn)率):預(yù)測值為1,且預(yù)測對了的比例,即:我們關(guān)注的那個事件,預(yù)測的有多準(zhǔn)。
召回率(查全率):所有真實(shí)值為1的數(shù)據(jù)中,預(yù)測對了的個數(shù),即:我們關(guān)注的那個事件真實(shí)的發(fā)生情況下,我們成功預(yù)測的比例是多少。
有的時候,對于一個算法而言,精準(zhǔn)率與召回率之間呈現(xiàn)負(fù)相關(guān)的關(guān)系。精準(zhǔn)率高一些,召回率就低一些;或者召回率高一些,精準(zhǔn)率就低一些。那么如何取舍呢?
其實(shí)在衡量機(jī)器學(xué)習(xí)的其他指標(biāo)中,我們也需要進(jìn)行取舍,通常只需要把握一個原則:
視場景而定。
比如我們做了一個股票預(yù)測系統(tǒng),未來股票是??還是??這樣一個二分類問題。很顯然“漲”才是我們關(guān)注的焦點(diǎn),那么我們肯定希望:系統(tǒng)預(yù)測上漲的股票中,真正上漲的比例越大越好,這就是希望查準(zhǔn)率高。那么我們是否關(guān)注查全率呢?在大盤中有太多的真實(shí)上漲股票,雖然我們漏掉了一些上升周期,但是我們沒有買進(jìn),也就沒有損失。但是如果查準(zhǔn)率不高,預(yù)測上漲的結(jié)果下跌了,那就是實(shí)實(shí)在在的虧錢了。所以在這個場景中,查準(zhǔn)率更重要。
當(dāng)然也有追求召回率的場景,在醫(yī)療領(lǐng)域做疾病診斷,如果召回率低,意味著本來有一個病人得病了,但是沒有正確預(yù)測出來,病情就惡化了。我們希望盡可能地將所有有病的患者都預(yù)測出來,而不是在看在預(yù)測有病的樣例中有多準(zhǔn)。
五、F1 Score
5.1、F1 Score 的定義
在實(shí)際業(yè)務(wù)場景中,也有很多沒有這么明顯的選擇。那么在同時需要關(guān)注精準(zhǔn)率和召回率,如何在兩個指標(biāo)中取得平衡呢?在這種情況下,我們使用一種新的指標(biāo):F1 Score。
如果要我們綜合精準(zhǔn)率和召回率這兩個指標(biāo),我們可能會想到取平均值這樣的方法。F1 Score的思想也差不多:
F1 Score 是精準(zhǔn)率和召回率的調(diào)和平均值。
什么是調(diào)和平均值?為什么要取調(diào)和平均值?調(diào)和平均值的特點(diǎn)是如果二者極度不平衡,如某一個值特別高、另一個值特別低時,得到的F1 Score值也特別低;只有二者都非常高,F(xiàn)1才會高。這樣才符合我們對精準(zhǔn)率和召回率的衡量標(biāo)準(zhǔn)。
5.2、代碼演示
def f1_score(precision, recall):
try:
return 2 * precision * recall / (precision + recall)
except:
return 0.0
precision = 0.5
recall = 0.5
f1_score(precision, recall)
0.5
假設(shè)精準(zhǔn)率和召回率同時為0.5,則二者的算數(shù)平均值為0.5,計(jì)算F1 Score:
precision = 0.5
recall = 0.5
f1_score(precision, recall)
0.5
假設(shè)精準(zhǔn)率為0.9,召回率同時為0.1,則二者的算數(shù)平均值為0.5,計(jì)算F1 Score:
precision = 0.5
recall = 0.5
f1_score(precision, recall)
0.5
六、ROC曲線
6.1、分類閾值、TPR和FPR
在了解ROC曲線之前,先看三個概念:分類閾值、TPR和FPR
6.1.1、分類閾值
分類閾值,即設(shè)置判斷樣本為正例的閾值threshold,
如果某個邏輯回歸模型對某封電子郵件進(jìn)行預(yù)測時返回的概率為 0.9995,則表示該模型預(yù)測這封郵件非??赡苁抢]件。相反,在同一個邏輯回歸模型中預(yù)測分?jǐn)?shù)為 0.0003 的另一封電子郵件很可能不是垃圾郵件??扇绻撤怆娮余]件的預(yù)測分?jǐn)?shù)為 0.6 呢?為了將邏輯回歸值映射到二元類別,您必須指定分類閾值(也稱為判定閾值)。如果值高于該閾值,則表示“垃圾郵件”;如果值低于該閾值,則表示“非垃圾郵件”。人們往往會認(rèn)為分類閾值應(yīng)始終為 0.5,但閾值取決于具體問題,因此必須對其進(jìn)行調(diào)整。
在sklearn中有一個方法叫:decision_function,即返回分類閾值
decision_scores = log_reg.decision_function(X_test)
y_predict = np.array(decision_scores >= 5, dtype='int')
# decision_scores
# X_test
array([[ 0., 0., 4., ..., 3., 0., 0.],
[ 0., 0., 4., ..., 2., 0., 0.],
[ 0., 2., 11., ..., 10., 0., 0.],
...,
[ 0., 0., 1., ..., 0., 0., 0.],
[ 0., 0., 0., ..., 0., 0., 0.],
[ 0., 0., 12., ..., 8., 0., 0.]])
我們知道,精準(zhǔn)率和召回率這兩個指標(biāo)有內(nèi)在的聯(lián)系,并且相互沖突。precision隨著threshold的增加而降低,recall隨著threshold的增大而減小。如果某些場景需要precision,recall都保持在80%,可以通過這種方式求出threshold
6.1.2、TPR
| 預(yù)測值0 | 預(yù)測值1 | |
|---|---|---|
| 真實(shí)值0 | TN | FP |
| 真實(shí)值1 | FN | TP |
TPR:預(yù)測為1,且預(yù)測對了的數(shù)量,占真實(shí)值為1的數(shù)據(jù)百分比。很好理解,就是召回率。
6.1.3、FPR
FPR:預(yù)測為1,但預(yù)測錯了的數(shù)量,占真實(shí)值不為1的數(shù)據(jù)百分比。與TPR相對應(yīng),F(xiàn)PR除以真實(shí)值為0的這一行所有的數(shù)字和 。
| 預(yù)測值0 | 預(yù)測值1 | |
|---|---|---|
| 真實(shí)值0 | 9978 | 12 |
| 真實(shí)值1 | 2 | 8 |
TPR和FPR之間是成正比的,TPR高,F(xiàn)PR也高。ROC曲線就是刻畫這兩個指標(biāo)之間的關(guān)系。
6.2、 什么是ROC曲線
ROC曲線(Receiver Operation Characteristic Cureve),描述TPR和FPR之間的關(guān)系。x軸是FPR,y軸是TPR。
我們已經(jīng)知道,TPR就是所有正例中,有多少被正確地判定為正;FPR是所有負(fù)例中,有多少被錯誤地判定為正。 分類閾值取不同值,TPR和FPR的計(jì)算結(jié)果也不同,最理想情況下,我們希望所有正例 & 負(fù)例 都被成功預(yù)測 TPR=1,F(xiàn)PR=0,即 所有的正例預(yù)測值 > 所有的負(fù)例預(yù)測值,此時閾值取最小正例預(yù)測值與最大負(fù)例預(yù)測值之間的值即可。
TPR越大越好,F(xiàn)PR越小越好,但這兩個指標(biāo)通常是矛盾的。為了增大TPR,可以預(yù)測更多的樣本為正例,與此同時也增加了更多負(fù)例被誤判為正例的情況。
6.2.1、代碼實(shí)現(xiàn)
import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
digits = datasets.load_digits()
X = digits.data
y = digits.target.copy()
# 要構(gòu)造偏斜數(shù)據(jù),將數(shù)字9的對應(yīng)索引的元素設(shè)置為1,0~8設(shè)置為0
y[digits.target==9]=1
y[digits.target!=9]=0
# 使用邏輯回歸做一個分類
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=666)
log_reg = LogisticRegression()
log_reg.fit(X_train,y_train)
# 計(jì)算邏輯回歸給予X_test樣本的決策數(shù)據(jù)值
# 通過decision_function可以調(diào)整精準(zhǔn)率和召回率
decision_scores = log_reg.decision_function(X_test)
# print(decision_scores)
# TPR
def TPR(y_true, y_predict):
tp = TP(y_true, y_predict)
fn = FN(y_true, y_predict)
try:
return tp / (tp + fn)
except:
return 0.0
# FPR
def FPR(y_true, y_predict):
fp = FP(y_true, y_predict)
tn = TN(y_true, y_predict)
try:
return fp / (fp + tn)
except:
return 0.0
fprs = []
tprs = []
# 以0.1為步長,遍歷decision_scores中的最小值到最大值的所有數(shù)據(jù)點(diǎn),將其作為閾值集合
thresholds = np.arange(np.min(decision_scores), np.max(decision_scores), 0.1)
print(thresholds, len(thresholds))
for threshold in thresholds:
# decision_scores >= threshold 是布爾型向量,用dtype設(shè)置為int
# 大于等于閾值threshold分類為1,小于為0,用這種方法得到預(yù)測值
y_predict = np.array(decision_scores >= threshold, dtype=int)
#print(y_predict)
# print(y_test)
#print(FPR(y_test, y_predict))
# 對于每個閾值,所得到的FPR和TPR都添加到相應(yīng)的隊(duì)列中
fprs.append(FPR(y_test, y_predict))
tprs.append(TPR(y_test, y_predict))
# 繪制ROC曲線,x軸是fpr的值,y軸是tpr的值
plt.plot(fprs, tprs)
plt.show()
[-85.68608523 -85.58608523 -85.48608523 ... 19.61391477 19.71391477
19.81391477] 1056

可以看到曲線每次都是一個“爬坡”,遇到正例往上爬一格,錯了往右爬一格,顯然往上爬對于算法性能來說是最好的。
sklearn中的ROC曲線:
from sklearn.metrics import roc_curve
fprs, tprs, thresholds = roc_curve(y_test, decision_scores)
plt.plot(fprs, tprs)
plt.show()

6.2.2 分析
ROC曲線距離左上角越近,證明分類器效果越好。如果一條算法1的ROC曲線完全包含算法2,則可以斷定性能算法1>算法2。這很好理解,此時任做一條 橫線(縱線),任意相同TPR(FPR) 時,算法1的FPR更低(TPR更高),故顯然更優(yōu)。
從上面ROC圖中的幾個標(biāo)記點(diǎn),我們可以做一些直觀分析:
我們可以看出,左上角的點(diǎn)(TPR=1,FPR=0),為完美分類,也就是這個醫(yī)生醫(yī)術(shù)高明,診斷全對。點(diǎn)A(TPR>FPR),說明醫(yī)生A的判斷大體是正確的。中線上的點(diǎn)B(TPR=FPR),也就是醫(yī)生B全都是蒙的,蒙對一半,蒙錯一半;下半平面的點(diǎn)C(TPR<FPR),這個醫(yī)生說你有病,那么你很可能沒有病,醫(yī)生C的話我們要反著聽,為真庸醫(yī)。
很多時候兩個分類器的ROC曲線交叉,無法判斷哪個分類器性能更好,這時可以計(jì)算曲線下的面積AUC,作為性能度量。
七、AUC
一般在ROC曲線中,我們關(guān)注是曲線下面的面積, 稱為AUC(Area Under Curve)。這個AUC是橫軸范圍(0,1 ),縱軸是(0,1)所以總面積是小于1的。
ROC和AUC的主要應(yīng)用:比較兩個模型哪個好?主要通過AUC能夠直觀看出來。
ROC曲線下方由梯形組成,矩形可以看成特征的梯形。因此,AUC的面積可以這樣算:(上底+下底)* 高 / 2,曲線下面的面積可以由多個梯形面積疊加得到。AUC越大,分類器分類效果越好。
- AUC = 1,是完美分類器,采用這個預(yù)測模型時,不管設(shè)定什么閾值都能得出完美預(yù)測。絕大多數(shù)預(yù)測的場合,不存在完美分類器。
- 0.5 < AUC < 1,優(yōu)于隨機(jī)猜測。這個分類器(模型)妥善設(shè)定閾值的話,能有預(yù)測價值。
- AUC = 0.5,跟隨機(jī)猜測一樣,模型沒有預(yù)測價值。
- AUC < 0.5,比隨機(jī)猜測還差;但只要總是反預(yù)測而行,就優(yōu)于隨機(jī)猜測。
可以在sklearn中求出AUC值
from sklearn.metrics import roc_auc_score
roc_auc_score(y_test, decision_scores)
0.9830452674897119