前面我们所讲的对分类算法的评价,使用的是准确率的方式来衡量算法的好坏,公式为:sum(y_true == y_predict) / len(y_true)
,但是这种衡量方式有一种弊端,当数据特别极限偏斜的情况下,这种衡量方式就不太准确了,那么具体有什么陷阱呢?且听我慢慢道来。
1 精准率和召回率
1.1 准确率方式的陷阱
准确率公式:sum(y_true == y_predict) / len(y_true)
结论:当数据极度倾斜的情况下,准确率来衡量是不准确的。
举个例子?
我们做一个癌症预测系统,输入体检信息,可以判断是否有癌症。如果我们的算法预测准确率是99.9%,那么这个系统是好?还是坏?
答:不一定好。如果癌症产生的概率只有0.1%。假如我们系统预测所有的人都是健康的,这样我们的预测准确率也是99.9%,也就是白忙活一场。而如果癌症产生的概率只有0.01%,那我们的算法还不如个傻子预测的准。
那么该用什么来衡量分类算法的好坏呢?首先我们先介绍几个概念。
1.2 混淆矩阵
对于二分类的问题,我们构造这样的一个矩阵。

- 行代表真实值,列代表预测值。
- TN:实际为0,预测也为0,True Negative
- FP:实际为0,但是预测为1,False Positive
- FN:实际为1,但是预测为0,False Negative
- TP:实际为1,预测也为1,True Positive
举例:假设癌症系统预测10000个人,混淆矩阵如下所示:

- 10000个人中,9978个人本没患癌症,同时算法预测也没患癌症。
- 同时有12个人本来没患癌症,但是预测算法预测患了癌症。
- 有2个人确实患了癌症,但是算法预测他没有患癌症。
- 有8个人患了癌症,同时算法也预测他患了癌症。
1.3 精准率和召回率

在实际的有偏数据中,1通常是我们关注的那个事件
因此精准率就是:预测我们关注的那个事件,有多准
举例:
癌症系统:有偏数据中,1是患癌症的事件,在预测有癌症的人群中,有40%的人是预测对的。
金融系统:有偏数据中,1是股票上涨,我们预测了20个股票上涨,有8个是预测对的。

表示:我们关注的那个事件,真实的发生了的这些数据中,我们成功预测了多少。
举例:
癌症系统:真实患癌症的人中,我们预测成功了80%。真实有10个人患癌症的人,我们召回了8个人。
金融系统:实际股票上涨的10只股票,我们预测对了8只,即召回了8只。
通过介绍上面两个评价方式,此时再回到开始提到的准确率有陷阱的问题中:10000个人预测都是健康的,实际患癌症的概率为0.1%。此时准确率为99.9%

1.4 代码实现
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
2 精准率与召回率平衡
有的精准率高,召回率低,有些精准率低,召回率高,我们该如何取舍呢?
对于我们刚刚举的2个例子,预测癌症与预测股票。
预测股票的时候,我们对精准率更加关注些,此时我们不用太关注召回率,所有涨的股票中,漏掉几个也无伤大雅,反正也没投入,也没亏钱。反而如果预测的股票中,也就是我们投钱的股票中,跌的多,涨的少,那我们就亏钱了。因此精准率越高,我们就越赚钱。
预测癌症与之相反,我们对召回率关注更高。一些人本来没患癌症,我预测了你们患了癌症,这些人可能多参加几个检查就好了,也没什么生命风险,因此精准率低点无所谓。然而如果病人实际患了癌症,我预测他没患,那可能就会耽误病人的病情,可能会害了人家性命,那是万万不行的,因此对召回率有更高的要求。
但是还有一些场景,对精准率和召回率并不像上面两个例子那么极端,希望同时关注精准率和召回率,此时该怎么办呢?
2.1 F1 Score
二者都兼顾,就使用precision和recall的调和平均值—— F1 Score
那么为什么使用调和平均值,而不直接使用二者的平均值呢?
答:因为调和平均值有一个性质,当二者一个非常小一个非常大时,F1也会很小。只有当二者都很大时,F1才会大。
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.
2.2 Precision-Recall的平衡

在逻辑回归中,决策边界为,那么如果将决策边界进行平移,
,如果大于threshold分类为1,小于threshold分类为0
举例说明:其中五角星实际分类为1,圆圈实际分类为0,对于决策边界的不同,对应的精准率与召回率也不相同。

可以看到这样的特性,随着threshold增大,精准率是逐渐增大的,召回率逐渐降低,那么此时可以找到这样一个超参数threshold,使得Precision-Recall平衡
log_reg = LogisticRegression()
log_reg.fit(X_train, y_train)
y_predict = log_reg.predict(X_test)
#通过此方法,可以获取决策分数值
decision_scores = log_reg.decision_function(X_test) # array([-22.05700185, -33.02943631, -16.21335414, -80.37912074,-48.25121102, -24.54004847, -44.39161228, -25.0429358 ,-0.97827574, -19.71740779])
# 设置threshold = 5
y_predict_2 = np.array(decision_scores >= 5, dtype='int')
precision_score(y_test, y_predict_2)
recall_score(y_test, y_predict_2)
2.3 Precision-Recall曲线
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
from sklearn.metrics import precision_score
from sklearn.metrics import recall_score
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)
log_reg = LogisticRegression()
log_reg.fit(X_train, y_train)
decision_scores = log_reg.decision_function(X_test)
# 绘制曲线
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)
plt.plot(thresholds, recalls)
plt.show()

也可以这样绘制
plt.plot(precisions, recalls)
plt.show()

此时箭头处是一个急剧下降的点,那么此处可能就是Precision-Recall平衡的点。
scikit-learn中提供更简便的方式
from sklearn.metrics import precision_recall_curve
# 一行代码就返回了三个值
precisions, recalls, thresholds = precision_recall_curve(y_test, decision_scores)
那么如果我们通过两次调参,分别获得如下的曲线,那么说明外侧曲线的模型更好一些,也可以通过面积来看,不过通常会使用下面ROC曲线的面积,来衡量我们的算法。

3 ROC曲线
3.1 TPR与FPR
先来看几个定义:
TPR: 与召回率相同

FPR: 预测为1,可惜预测错了,真实值不为1的概率

TPR与FPR呈现相一致的趋势
举例说明:其中五角星为真实分类为1,圆圈真实分类为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.
3.2 ROC曲线绘制
log_reg = LogisticRegression()
log_reg.fit(X_train, y_train)
decision_scores = log_reg.decision_function(X_test)
fprs = []
tprs = []
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')
fprs.append(FPR(y_test, y_predict))
tprs.append(TPR(y_test, y_predict))
plt.plot(fprs, tprs)
plt.show()

scikit-learn中的ROC
from sklearn.metrics import roc_curve
# 也是一行搞定
fprs, tprs, thresholds = roc_curve(y_test, decision_scores)
3.3 ROC AUC
其中ROC下面的面积取值是在0~1之间,可以使用ROC下的面积来衡量算法的好坏
from sklearn.metrics import roc_auc_score
# 传入的参数是decision_scores,内部已经封装好了,可以理解为先求出ROC曲线,然后再求面积
roc_auc_score(y_test, decision_scores) # 0.98304526748971188
ROC-ACU对有偏数据并不敏感,ROC主要应用场合是比较两个模型孰优孰劣,面积大的模型更好一些。

4 多分类的问题
from sklearn.metrics import precision_score
# 默认是二分类的,多类别会报错
precision_score(y_test, y_predict)
#合理设置一些参数,可以解决多分类的问题
precision_score(y_test, y_predict, average="micro")
from sklearn.metrics import confusion_matrix
confusion_matrix(y_test, y_predict)

直观的方式,用图像绘制出来,数值越大,图片越亮
cfm = confusion_matrix(y_test, y_predict)
plt.matshow(cfm, cmap=plt.cm.gray) # 绘制矩阵,映射成灰度值
plt.show()
关注预测正确的是没有意义的,我们要看错误的地方
row_sums = np.sum(cfm, axis=1) # 每一行求和
err_matrix = cfm / row_sums # 错误占每行的百分比
np.fill_diagonal(err_matrix, 0) #正确的不关注
plt.matshow(err_matrix, cmap=plt.cm.gray)
plt.show()

上图说明(1,9)和(8,1)很亮,说明这两个犯得错误更多一些,
此时在算法层面上,可以单独调整一下这两个分类的模型,
另外也可以分析下样本数据,是否数据上有什么问题。
声明:此文章为本人学习笔记,课程来源于慕课网:python3入门机器学习经典算法与应用。在此也感谢bobo老师精妙的讲解。
如果您觉得有用,欢迎关注我的公众号,我会不定期发布自己的学习笔记、AI资料、以及感悟,欢迎留言,与大家一起探索AI之路。

网友评论