机器学习系列笔记十: 分类算法的衡量
文章目录
分类准确度的问题
对于前面总结的所有分类算法(KNN,Logistic Regression)我们都是使用分类准确度来衡量算法预测的好坏,其实现逻辑大概可以用如下代码表示:
import numpy as np
def accuracy_score(y_true,y_predict):
"""计算y_true和y_predict之间的准确率"""
assert y_true.shape[0] == y_predict.shape[0], \
"the size of y_true must be equal to y_predict"
return sum(y_true==y_predict)/len(y_true)
而事实是,分类准确度在评价分类算法的时候是有一个很重要的问题的,比如现在有一个机器学习的任务,任务是搭建一个癌症预测系统,输入体检信息,可以判断是否有癌症。
而我们通过分类准确度得到的结果是预测准确度达到了99.9%,是不是就说明这个模型非常好呢?
如果癌症产生的概率是0.1%,我们输入了1000名体检者的信息,按照概率计算这些体检者中有1个患者,999个非患者。
假定我们的模型不管是什么输入都一律输出为不患癌症,可以计算得到的预测准确率为 999 1000 = 99.9 % \frac{999}{1000}=99.9\% 1000999=99.9%,这显然是不合理的。
所以可以总结:对于极度偏斜(Skewed Data)或者叫数据倾斜的数据集,只使用分类准确度是远远不够的。
由此,我们需要其他的一些指标使得我们在使用极度偏斜的数据集进行预测的时候得到衡量结果能够真实反映模型的精度。通常使用混淆矩阵的数据手段来改进衡量指标。
混淆矩阵Confusion Matrix
对于二分类问题其混淆矩阵可以以如下形式表示
true\predict | 0 | 1 |
---|---|---|
0 | 预测negative正确(TN) | 预测Positive错误(FP) |
1 | 预测negative错误(FN) | 预测Positive正确(TP) |
其中行表示真实值,列表示预测值。
- 0-Negative
- 1-Positive:通常表示我们真正关系的那一类数据,通常也是小概率事件所代表的类别。
例子:同样对于癌症预测的样本,共有1000个,模型的预测结果就可以以混淆矩阵的方式给出
true\predict | 0 | 1 |
---|---|---|
0 | 9978(TN) | 12(FP) |
1 | 2(FN) | 8(TP) |
借助混淆矩阵这个数据工具我们可以把预测的结果与真实的数据以矩阵的方式给出,然后通过混淆矩阵的相关运算我们可以获得对于分类模型更好的评判指标,如精准率和召回率。
精准率和召回率
精准率
p
r
e
c
i
s
i
o
n
=
T
P
T
P
+
F
P
precision = \frac{TP}{TP+FP}
precision=TP+FPTP
对于前面模型癌症预测结果表征的混淆矩阵,由上式可以结算得到该模型的精准率为40%
true\predict | 0 | 1 |
---|---|---|
0 | 9978(TN) | 12(FP) |
1 | 2(FN) | 8(TP) |
精准率表示对于类别1(患病)模型的预测结果的准确度,往往类比1是我们比较关心的小概率事件。
召回率
r
e
c
a
l
l
=
T
P
T
P
+
F
N
recall = \frac{TP}{TP+FN}
recall=TP+FNTP
true\predict | 0 | 1 |
---|---|---|
0 | 9978(TN) | 12(FP) |
1 | 2(FN) | 8(TP) |
召回率表示每当有10个类别1(癌症患者),通过现有分类模型成功在样本中找到了8个(癌症患者),即recall=80%
精准率和召回率的不同关键在于分母不同,由于分母不同,所以对两个指标的解读就不同了
精准率和召回率的效用
同分类准确度问题中提到到癌症分类问题,假设现在有10000个人(10个人患了癌症),我们预测所有的人都是健康的,那么预测结果的混淆矩阵表示如下:
true\predict | 0 | 1 |
---|---|---|
0 | 9990(TN) | 0(FP) |
1 | 10(FN) | 0(TP) |
如果用分类准确度来衡量,模型的准确度高达 9990 / 10000 = 99.9 % 9990/10000=99.9\% 9990/10000=99.9%
如果用精准率衡量,结果为:0
如果用召回率衡量,结果为:0
可以看到通过使用精准率和召回率来判别分类系统的好坏可以有效避过数据偏斜所产生的衡量问题。
实现混淆矩阵、精准率和召唤率
准备数据
import warnings
import numpy as np
from sklearn import datasets
warnings.filterwarnings("ignore")
digits = datasets.load_digits()
X = digits.data
y = digits.target.copy()
使得数据集有较大的偏斜
y [digits.target==9] = 1
y [digits.target!=9] = 0
from sklearn.model_selection import train_test_split
X_train,X_test,y_train,y_test = train_test_split(X,y,random_state=666)
使用Logistic Regression完成分类任务
from sklearn.linear_model import LogisticRegression
log_reg = LogisticRegression()
log_reg.fit(X_train,y_train)
log_reg.score(X_test,y_test)
0.9755555555555555
使用其他衡量指标
- 计算混淆矩阵
y_predict = log_reg.predict(X_test)
def TN(y_true,y_pre):
assert len(y_true)==len(y_pre)
return np.sum((y_true == 0)&(y_pre == 0))
TN(y_test,y_predict)
403
def FP(y_true,y_pre):
assert len(y_true)==len(y_pre)
return np.sum((y_true == 0)&(y_pre == 1))
FP(y_test,y_predict)
2
def FN(y_true,y_pre):
assert len(y_true)==len(y_pre)
return np.sum((y_true == 1)&(y_pre == 0))
FN(y_test,y_predict)
9
def TP(y_true,y_pre):
assert len(y_true)==len(y_pre)
return np.sum((y_true == 1)&(y_pre == 1))
TP(y_test,y_predict)
36
true\predict | 0 | 1 |
---|---|---|
0 | 预测negative正确(TN) | 预测Positive错误(FP) |
1 | 预测negative错误(FN) | 预测Positive正确(TP) |
def confusion_matrix(y_true,y_pre):
return np.array([
[TN(y_true,y_pre),FP(y_true,y_pre)],
[FN(y_true,y_pre),TP(y_true,y_pre)]
])
confusion_matrix(y_test,y_predict)
array([[403, 2],
[ 9, 36]])
- 精准率
def precision_score(y_true,y_pre):
tp = confusion_matrix(y_true,y_pre)[1,1]
fp = confusion_matrix(y_true,y_pre)[0,1]
try:
return tp / (tp+fp)
except:
return 0.0
precision_score(y_test,y_predict)
0.9473684210526315
- 召回率
def recall_score(y_true,y_pre):
tp = confusion_matrix(y_true,y_pre)[1,1]
fn = confusion_matrix(y_true,y_pre)[1,0]
try:
return tp / (tp+fn)
except:
return 0.0
recall_score(y_test,y_predict)
0.8
scikit-learn中的混淆矩阵,精准率与召回率
- 混淆矩阵
from sklearn.metrics import confusion_matrix
confusion_matrix(y_test,y_predict)
array([[403, 2],
[ 9, 36]], dtype=int64)
- 精准率
from sklearn.metrics import precision_score
precision_score(y_test,y_predict)
0.9473684210526315
- 召回率
from sklearn.metrics import recall_score
recall_score(y_test,y_predict)
0.8
F1 Score
在前面我们了解并实现了精准率与召回率,但是这是两个指标的衡量结果会产生差异,对于同一个算法有的时候精准率高一些,有时候召回率高一些。具体在使用这两个指标的时候我们应该如何解读精准率和召回率应该是根据具体的场景而定。
有时候我们注重精准率。如股票预测,在雷达通信中称之为高漏警低虚警。
关注所有未来股票会升(Positive)的预测中有多少是对的,我们希望这个比例越高越好。
有时候我们注重召回率,如癌症预测,在雷达通信中称之为高虚警低漏警。
关注对于所有真正有病的患者的预测结果准确性,期望所有有病(negative)的患者都能被正确的预测
但是更多的时候我们是希望获得精准率和召回率之间的一个平衡,换言之我们希望同时关注精准率和召回率,所以有了一个新的指标:F1 Score:F1 Score 是precision与recall的调和平均值:
F
1
=
2
⋅
p
r
e
c
i
s
i
o
n
⋅
r
e
c
a
l
l
p
r
e
c
i
s
i
o
n
+
r
e
c
a
l
l
F1 = \frac{2\cdot precision \cdot recall}{precision+recall}
F1=precision+recall2⋅precision⋅recall
F1 Score的实现
import numpy as np
def f1_score(precision,recall):
try:
return 2 * precision * recall / (precision + recall)
except:
return 0.0
看一下如果precision和recall相等,F1 Score的结果是多少
precision = 0.5
recall = 0.5
f1_score(precision,recall)
0.5
如果precision和recall不等,F1 Score的结果情况
precision = 0.1
recall = 0.9
f1_score(precision,recall)
0.18000000000000002
precision = 0.0
recall = 1.
f1_score(precision,recall)
0.0
可以看到只有当recall与precision都比较大的时候,才会得到比较高F1_Score
接下来看一下在具体的数据中F1_Score是怎么样的
from sklearn import datasets
from sklearn.model_selection import train_test_split
digits = datasets.load_digits()
X = digits.data
y = digits.target.copy()
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)
import warnings
warnings.filterwarnings("ignore")
from sklearn.linear_model import LogisticRegression
log_reg = LogisticRegression()
log_reg.fit(X_train,y_train)
log_reg.score(X_test,y_test)
0.9755555555555555
可以看到分类准确率还是挺高的,然后我们看看F1 Score
from sklearn.metrics import f1_score
y_predict = log_reg.predict(X_test)
f1_score(y_test,y_predict)
0.8674698795180723
显然没有上面的分类准确度高,这是因为数据是有偏的。
Precision-Recall的平衡
本质上精准率和召回率是互相矛盾的,所以我们无法使得某个模型的这两个指标同时提升,但是我们可以根据特定的场景,特定的目标来调整二者的平衡的,按照我们的期望使得Precision比较高或者Recall比较高。
回顾逻辑回归的决策边界,我们通常是根据 θ T x b = 0 \theta^T x_b=0 θTxb=0 (表示以概率=0.5作为判定的基准)来求出来的,事实上我们可以通过改变0为其他的数使得决策边界对应地平移,从而得到不同地精准率和召回率。
其实上图中代表轴的黑线向右移,表示我们把判定为1的概率值不断上调,所以会得到较高的精准率。
反之向左移动,会得到较差的精准率,同时由于判定的阈值不断下降,越来越多的样本被判定为1,就会不断增大真实结果为1的样本被正确判定的概率,从而增大召回率。
更改判定阈值改变平衡点
import numpy as np
from sklearn import datasets
from sklearn.model_selection import train_test_split
digits = datasets.load_digits()
X = digits.data
y = digits.target.copy()
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)
我们可以通过decision_function得到每一个样本对应的预测值
log_reg.decision_function(X_test)[:10]
array([-22.05700117, -33.02940957, -16.21334087, -80.3791447 ,
-48.25125396, -24.54005629, -44.39168773, -25.04292757,
-0.97829292, -19.7174399 ])
log_reg.predict(X_test)[:10]
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
基于decision_function我们可以封装一个函数来实现对阈值(threshold)的控制
decision_scores = log_reg.decision_function(X_test)
np.max(decision_scores)
19.8895858799022
np.min(decision_scores)
-85.68608522646575
y_predict2 = np.array(decision_scores>=5 ,dtype=int)
from sklearn.metrics import recall_score
from sklearn.metrics import precision_score
print("threshold=0,recall_score:",recall_score(y_test,y_predict))
print("threshold=5,recall_score:",recall_score(y_test,y_predict2))
print("threshold=0,precision_score:",precision_score(y_test,y_predict))
print("threshold=5,precision_score:",precision_score(y_test,y_predict2))
threshold=0,recall_score: 0.8
threshold=5,recall_score: 0.5333333333333333
threshold=0,precision_score: 0.9473684210526315
threshold=5,precision_score: 0.96
y_predict3 = np.array(decision_scores>=-5 ,dtype=int)
print("threshold=0,recall_score:",recall_score(y_test,y_predict))
print("threshold=-5,recall_score:",recall_score(y_test,y_predict3))
print("threshold=0,precision_score:",precision_score(y_test,y_predict))
print("threshold=-5,precision_score:",precision_score(y_test,y_predict3))
threshold=0,recall_score: 0.8
threshold=-5,recall_score: 0.8888888888888888
threshold=0,precision_score: 0.9473684210526315
threshold=-5,precision_score: 0.7272727272727273
可视化阈值对精准率和召回率的影响
from sklearn.metrics import precision_score
from sklearn.metrics import recall_score
import matplotlib.pyplot as plt
precisions = []
recalls = []
thresholds = np.arange(np.min(decision_scores),np.max(decision_scores),0.1)
for threshold in thresholds:
y_predict = np.array(decision_scores>=threshold,dtype=int)
precisions.append(precision_score(y_test,y_predict))
recalls.append(recall_score(y_test,y_predict))
plt.plot(thresholds,precisions,label='precision')
plt.plot(thresholds,recalls,label='recall')
plt.legend()
plt.show()
假设我们希望精确率在%90以上,就可以通过计算蓝线y=0.9对应的x等于多少
Precision-Recall 曲线
plt.plot(precisions,recalls)
plt.xlabel("precision")
plt.ylabel("recall")
plt.show()
scikit-learn中的Precision-Recall曲线
from sklearn.metrics import precision_recall_curve
# 参数1:y_true,参数2:针对每个x的预测结果,会返回precisions,recalls以及thresholds
precisions,recalls,thresholds = precision_recall_curve(y_test,decision_scores)
precisions.shape
(145,)
recalls.shape
(145,)
thresholds.shape
(144,)
plt.plot(thresholds,precisions[:-1],label="precisions")
plt.plot(thresholds,recalls[:-1],label="recall")
plt.legend()
plt.show()
plt.plot(precisions,recalls)
plt.xlabel("precision")
plt.ylabel("recall")
plt.show()
ROC 曲线
true\predict | 0 | 1 |
---|---|---|
0 | 9978(TN) | 12(FP) |
1 | 2(FN) | 8(TP) |
这里首先给出两个新的指标:
T
P
R
=
R
e
c
a
l
l
=
T
P
T
P
+
F
N
TPR=Recall = \frac{TP}{TP+FN}
TPR=Recall=TP+FNTP
F P R = F P T N + F N FPR = \frac{FP}{TN+FN} FPR=TN+FNFP
我这里用红蓝来分别表示了TPR与FPR所设计的数值。
接下来看看TPR与FPR的关系:
在绘制ROC曲线之前,把所有本节相关的函数都封装到metrics模块中
import numpy as np
from math import sqrt
def accuracy_score(y_true, y_predict):
"""计算y_true和y_predict之间的准确率"""
assert len(y_true) == len(y_predict), \
"the size of y_true must be equal to the size of y_predict"
return np.sum(y_true == y_predict) / len(y_true)
def mean_squared_error(y_true, y_predict):
"""计算y_true和y_predict之间的MSE"""
assert len(y_true) == len(y_predict), \
"the size of y_true must be equal to the size of y_predict"
return np.sum((y_true - y_predict) ** 2) / len(y_true)
def root_mean_squared_error(y_true, y_predict):
"""计算y_true和y_predict之间的RMSE"""
return sqrt(mean_squared_error(y_true, y_predict))
def mean_absolute_error(y_true, y_predict):
"""计算y_true和y_predict之间的MAE"""
assert len(y_true) == len(y_predict), \
"the size of y_true must be equal to the size of y_predict"
return np.sum(np.absolute(y_true - y_predict)) / len(y_true)
def r2_score(y_true, y_predict):
"""计算y_true和y_predict之间的R Square"""
return 1 - mean_squared_error(y_true, y_predict) / np.var(y_true)
def TN(y_true, y_predict):
assert len(y_true) == len(y_predict)
return np.sum((y_true == 0) & (y_predict == 0))
def FP(y_true, y_predict):
assert len(y_true) == len(y_predict)
return np.sum((y_true == 0) & (y_predict == 1))
def FN(y_true, y_predict):
assert len(y_true) == len(y_predict)
return np.sum((y_true == 1) & (y_predict == 0))
def TP(y_true, y_predict):
assert len(y_true) == len(y_predict)
return np.sum((y_true == 1) & (y_predict == 1))
def confusion_matrix(y_true, y_predict):
return np.array([
[TN(y_true, y_predict), FP(y_true, y_predict)],
[FN(y_true, y_predict), TP(y_true, y_predict)]
])
def precision_score(y_true, y_predict):
assert len(y_true) == len(y_predict)
tp = TP(y_true, y_predict)
fp = FP(y_true, y_predict)
try:
return tp / (tp + fp)
except:
return 0.0
def recall_score(y_true, y_predict):
assert len(y_true) == len(y_predict)
tp = TP(y_true, y_predict)
fn = FN(y_true, y_predict)
try:
return tp / (tp + fn)
except:
return 0.0
def f1_score(y_true, y_predict):
precision = precision_score(y_true, y_predict)
recall = recall_score(y_true, y_predict)
try:
return 2. * precision * recall / (precision + recall)
except:
return 0.
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.
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.
然后我们通过jupter Notebook绘制ROC曲线
ROC曲线的绘制
import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
import warnings
warnings.filterwarnings("ignore")
digits = datasets.load_digits()
X = digits.data
y = digits.target.copy()
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)
使用逻辑回归模型
from sklearn.linear_model import LogisticRegression
log_reg = LogisticRegression()
log_reg.fit(X_train,y_train)
decision_scores = log_reg.decision_function(X_test)
建立画图所需坐标点
from relatedCode.metrics import FPR,TPR
fprs = []
tprs = []
thresholds = np.arange(np.min(decision_scores),np.max(decision_scores),dtype=int)
for threshold in thresholds:
y_predict = np.array(decision_scores>=threshold,dtype=int)
fprs.append(FPR(y_test,y_predict))
tprs.append(TPR(y_test,y_predict))
plt.plot(fprs,tprs)
plt.xlabel("fpr")
plt.ylabel("tpr")
plt.show()
scikit-learn 中的ROC
from sklearn.metrics import roc_curve
fprs,tprs,thresholds = roc_curve(y_test,decision_scores)
plt.plot(thresholds,fprs,label="fprs")
plt.plot(thresholds,tprs,label="tprs")
plt.xlabel("threshold")
plt.legend()
plt.show()
plt.plot(fprs,tprs)
plt.show()
scikit learn还提供了计算上图曲线下面积的方法
from sklearn.metrics import roc_auc_score
roc_auc_score(y_test,decision_scores)
0.9830452674897119
根据积分的定义或者单纯从上图可以看到,通过比较曲线下面积的方法可以反映模型之间针对当前场景的好坏(面积越大越好)
多分类问题中的混淆矩阵
在前面对于极度有偏的数据相应的一些新的分类指标做了讨论,但是都是基于二分类问题的。
在此将对多分类问题,我们如何利用前面二分类指标-混淆矩阵进行度量数据偏移的情况下的多分类算法进行讨论,
import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
import warnings
warnings.filterwarnings("ignore")
digits = datasets.load_digits()
X = digits.data
y = digits.target
X_train,X_test,y_train,y_test = train_test_split(X,y,random_state=666)
from sklearn.linear_model import LogisticRegression
log_reg = LogisticRegression()
log_reg.fit(X_train,y_train)
log_reg.score(X_test,y_test)
0.9555555555555556
我们看看对于多分类问题能不能通过precision_score求出模型的精准率
y_predict = log_reg.predict(X_test)
from sklearn.metrics import precision_score
precision_score(y_test,y_predict)
ValueError: Target is multiclass but average='binary'. Please choose another average setting, one of [None, 'micro', 'macro', 'weighted'].
报错:Target is multiclass but average=‘binary’,说明默认参数为average=‘binary’,无法解决多分类问题,然后尝试将参数改为average=‘micro’,重新试试
precision_score(y_test,y_predict,average='micro')
0.9555555555555556
同理,可以计算F1 score
from sklearn.metrics import f1_score
f1_score(y_test,y_predict,average='micro')
0.9555555555555556
最后我们查看该多分类问题预测的混淆矩阵
from sklearn.metrics import confusion_matrix
confusion_matrix(y_test,y_predict)
array([[45, 0, 0, 0, 0, 1, 0, 0, 0, 0],
[ 0, 37, 0, 0, 0, 0, 0, 0, 3, 0],
[ 0, 0, 49, 1, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 49, 0, 1, 0, 0, 3, 0],
[ 0, 1, 0, 0, 47, 0, 0, 0, 0, 0],
[ 0, 0, 0, 1, 0, 36, 0, 0, 1, 0],
[ 0, 0, 0, 0, 0, 1, 38, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 42, 0, 1],
[ 0, 2, 0, 0, 0, 0, 0, 0, 46, 0],
[ 0, 1, 0, 1, 1, 1, 0, 0, 0, 41]], dtype=int64)
绘制该混淆矩阵
cfm = confusion_matrix(y_test,y_predict)
plt.matshow(cfm,cmap=plt.cm.gray)
plt.show()
可以看到预测正确的个数越多,对应的cell就越亮,由于我们关注的是错误所对应的cell,所以需要对原混淆矩阵做一些处理
row_sums = np.sum(cfm,axis=1) # 求出每一行的和
err_matrix = cfm/row_sums
np.fill_diagonal(err_matrix,0) # 将对角线元素改为0
plt.matshow(err_matrix,cmap=plt.cm.gray)
plt.show()
可以看到越亮的地方对应预测错误次数越多。
- 我们的模型将很多
1
和3
预测成了8
- 把很多
8
预测成了1
总结
参考致谢
liuyubobo:https://github.com/liuyubobobo/Play-with-Machine-Learning-Algorithms
liuyubobo:https://coding.imooc.com/class/169.html
莫烦Python:https://www.bilibili.com/video/BV1xW411Y7Qd?from=search&seid=11773783205368546023