機(jī)器學(xué)習(xí)基礎(chǔ)-如何評價分類結(jié)果的優(yōu)劣?

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'])
pandas輸出混淆矩陣

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')
seaborn構(gòu)建的混淆矩陣

四、精準(zhǔn)率與召回率

精準(zhǔn)率:precision= \frac{TP}{TP+FP}

即精準(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)。

召回率:recall= \frac{TP}{TP+FN}

即精準(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)和平均值。

F1= \frac{2*precision*recall}{precision+recall}

什么是調(diào)和平均值?為什么要取調(diào)和平均值?調(diào)和平均值的特點(diǎn)是如果二者極度不平衡,如某一個值特別高、另一個值特別低時,得到的F1 Score值也特別低;只有二者都非常高,F(xiàn)1才會高。這樣才符合我們對精準(zhǔn)率和召回率的衡量標(biāo)準(zhǔn)。

\frac{1}{F1}= \frac{1}{2}(\frac{1}{precision} + \frac{1}{recall})

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ù)百分比。很好理解,就是召回率。
TPR = recall= \frac{TP}{TP+FN}

6.1.3、FPR

FPR:預(yù)測為1,但預(yù)測錯了的數(shù)量,占真實(shí)值不為1的數(shù)據(jù)百分比。與TPR相對應(yīng),F(xiàn)PR除以真實(shí)值為0的這一行所有的數(shù)字和 。
FPR = \frac{FP}{TN+FP}

預(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

八、總結(jié)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容