如果现在有一个癌症预测系统,输入患者的信息,可以判断是否有癌症。如果只使用分类准确度来评价模型的好坏是否合理?假如此时模型的预测准确度是99.9%,那么是否能认为模型是好的呢?如果癌症产生的概率只有0.1%,那就意味着这个癌症预测系统只有预测所有人都是健康,即可达到99.9%的准确率。那么此时还认为模型是好的嘛?假如更加极端一点,如果癌症产生的概率只有0.01%,那就意味着这个癌症预测系统只有预测所有人都是健康,即可达到99.99%的准确率。到这里,就能大概理解分类准确度评价模型存在的问题。什么时候才会出现这样的问题呢?这就是对于极度偏斜的数据(Skewed Data),也就样本数据极度不平衡的情况下,只使用分类准确度是远远不够的。因此需要引进更多的指标。
首先使用混淆矩阵(Confusion Matrix) 做进一步的分析。首先针对二分类问题,进行混淆矩阵分析。我们通过样本的采集,能够直接知道真实情况下,哪些数据结果是 positive,哪些结果是 negative。
混淆矩阵也称误差矩阵,是表示精度评价的一种标准格式,用n行n列的矩阵形式来表示。
介绍几个概念:
TN(True Negative):真实值Negative,预测Negative
FP(False Positive): 真实值Negative,预测Positive
FN(False Negative):真实值Positive,预测Negative
TP(True Positive): 真实值Positive,预测Positive
其实,就是希望右斜对角线越多越好就,即TN和TP的数量越多越好,也由此会延伸出更多的二级指标。
假设还是癌症预测,先测试了10000个人,预测结果如下:
TN:9978个人真实没有癌症,预测没有癌症;FP:12个人真实没有癌症,预测有癌症;FN:2个人真实有癌症,预测没有癌症;TP:8个人真实有癌症,预测也有癌症。
首先介绍由混淆矩阵延伸出来的两个二级指标:
由第一节中的实际案例,精准率=8/(8+12)=40%,这是因为通常在有偏的样本集中更加关注重点。精准率就是对更加关注的事件进行一个评判,比如例子中我们把预测有癌症作为关注的重点,就是在预测患有癌症的患者中真实患有癌症的概率。
由第一节中的实际案例,召回率=8/(8+2)=80%,就是在10个癌症患者中预测出了8个,80%就是召回率。
首先生成样本不均衡的数据:
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) 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 log_reg_predict = log_reg.predict(x_test) 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)) TN(y_test, log_reg_predict) # 403 FP(y_test, log_reg_predict) # 2 FN(y_test, log_reg_predict) # 9 TP(y_test, log_reg_predict) # 36 # 混淆矩阵的实现 def confusion_matrix(y_true, y_predict): return np.array([ [TN(y_test, log_reg_predict), FP(y_test, log_reg_predict)], [FN(y_test, log_reg_predict), TP(y_test, log_reg_predict)], ]) confusion_matrix(y_test, log_reg_predict) # array([[403, 2], # [ 9, 36]]) # 精准率 def precison_score(y_true, y_predict): tp = TP(y_test, log_reg_predict) fp = FP(y_test, log_reg_predict) try: return tp/(tp+fp) except: return 0.0 # 召回率 def recall_score(y_true, y_predict): tp = TP(y_test, log_reg_predict) fn = FN(y_test, log_reg_predict) try: return tp/(tp+fn) except: return 0.0 precison_score(y_test, log_reg_predict) # 0.9473684210526315 recall_score(y_test, log_reg_predict) # 0.8
from sklearn.metrics import confusion_matrix from sklearn.metrics import precision_score from sklearn.metrics import recall_score confusion_matrix(y_test, log_reg_predict) # array([[403, 2], # [ 9, 36]], dtype=int64) precision_score(y_test, log_reg_predict) # 0.9473684210526315 recall_score(y_test, log_reg_predict) # 0.8
在现实的使用过程中,这两个评价指标可能会出现一些矛盾,比如有些时候使用这种方法精准率高但召回率低,使用另外一种方法精准率低召回率高,那么如何权衡两种指标呢?
有时候比较注重精准率,比如股票预测,有时候更加注重召回率,比如病人诊断。对于不同的应用场景,偏好不同的指标。而往往有些时候可能并不是这么的极端,既需要保证精准率又需要保证召回率?由此引出一个新的指标:F1-score。
F1分数(F1 Score),是统计学中用来衡量二分类模型精确度的一种指标。它同时兼顾了分类模型的精确率和召回率。F1分数可以看作是模型精确率和召回率的一种调和平均,它的最大值是1,最小值是0。
编程实现:
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) log_reg.score(x_test, y_test) # 0.9755555555555555 log_reg_predict = log_reg.predict(x_test) def f1_score(precision, recall): try: return 2 * precision * recall / (precision + recall) except: return 0.0 precision_score(y_test, log_reg_predict) # 0.9473684210526315 recall_score(y_test, log_reg_predict) # 0.8 f1_score(precision_score(y_test, log_reg_predict), recall_score(y_test, log_reg_predict)) # 0.8674698795180723
sklearn中的实现:
from sklearn.metrics import f1_score f1_score(y_test, log_reg_predict) # 0.8674698795180723
通过上述例子进行一个对比:准确度:0.9755555555555555,精准率:0.9473684210526315,召回率:0.8,调和平均值f1-score:0.8674698795180723。精准率和召回率任何一个值比较低就会拉低整体分数。
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 precision = 0.1 recall = 0.9 f1_score(precision, recall) # 0.18000000000000002
其实这是一对矛盾的指标,精准率高召回率就低,精准率低召回率就高,那么如何平衡呢?首先回顾一下逻辑回归算法
决策边界:
解析几何中,其实这就是一条直线,这条直线就是分类中的决策边界,在直线的一侧为0,另一侧为1,那么为什么要取0呢?如果取任意值呢?
决策边界:
此时就是相当于平移决策边界,从而影响分类结果。
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 confusion_matrix 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) # 默认使用0作为决策边界。那如何对决策边界进行平移呢? y_predict = log_reg.predict(x_test) confusion_matrix(y_test, y_predict) # array([[403, 2], # [ 9, 36]], dtype=int64) precision_score(y_test, y_predict) # 0.9473684210526315 recall_score(y_test, y_predict) # 0.8
首先我们要知道预测结果中的最大值最小值。然后就可以根据自己的需求选择合适的threshold,对数据进行预测。
log_reg.decision_function(x_test) # 太多了,显示前10个 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])
通过前10个可以发现都是负数,预测结果都是0,这是因为predict默认使用0作为分类边界,小于0的都为0,大于0的为1。
decision_score = log_reg.decision_function(x_test) np.min(decision_score) # -85.68608522646575 np.max(decision_score) # 19.8895858799022
首先选择threshold=5,
y_predict2 = np.array(decision_score >= 5, dtype='int') confusion_matrix(y_test, y_predict2) # array([[404, 1], # [ 21, 24]], dtype=int64) precision_score(y_test, y_predict2) # 0.96 recall_score(y_test, y_predict2) # 0.5333333333333333
如果选择threshold=-5呢?
y_predict3 = np.array(decision_score >= -5, dtype='int') confusion_matrix(y_test, y_predict3) # array([[390, 15], # [ 5, 40]], dtype=int64) precision_score(y_test, y_predict3) # 0.7272727272727273 recall_score(y_test, y_predict3) # 0.8888888888888888
至此,使用decision_function这个函数改变了逻辑回归分类的阈值,相应的可以对比不同阈值情况下精准率和召回率的制约关系,那么具体做一个分类算法的时候,如何选取这个threshold去平衡精准率和召回率呢?由此引出精准率与召回率曲线。
PR曲线的两个指标都聚焦于正例。
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() # 生成不平衡的数据 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) from sklearn.metrics import precision_score from sklearn.metrics import recall_score precisions = [] recalls = [] thresholds = np.arange(np.min(decision_scores), np.max(decision_scores)) 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%以上,能有多少召回率?从而确定合适的阈值。
plt.plot(precisions, recalls) plt.show()
通过图中的趋势很显然随着精准率的提高,召回率在不断的下降。这也再一次印证了精准流程和召回率是互相制约互相平衡的,而在图中急剧下降的点大概就是精准率和召回率平衡的最佳点。
from sklearn.metrics import precision_recall_curve precisions, recalls, thresholds = precision_recall_curve(y_test, decision_scores) precisions.shape # (145,) recalls.shape # (145,) thresholds.shape # (144,)
通过上面程序输出结果可以发现返回的精准率和召回率与阈值的长度不一致,这是因为在sklearn中会自动取合适的阈值范围内计算准确率和召回率,而且默认的最大值为1和最小值为0,没有对应的threshold,因此这就是为什么thresholds比precisions和recalls长度少1,因此在绘图的时候需要注意。
plt.plot(thresholds, precisions[:-1]) plt.plot(thresholds, recalls[:-1]) plt.show()
通过这两条曲线对比自己编程实现的精准率和召回率曲线大致相同,有着略微的差异,这是因为sklearn中对阈值进行了处理,会自动选取最重要的那部分。
plt.plot(precisions, recalls) plt.show()
通过精准率和召回率曲线,可以确定合理的阈值去平衡精准率与召回率他们之间的变化关系。而PR曲线中急剧下降的点大概就是最佳平衡点。最后假如使用两种不同的算法绘制出的PR曲线如下图所示,那么哪种算法更优呢?
很显然,外面那根曲线上的每一个点都比里面那根曲线的precisions和recalls大,所以整体来说如果PR曲线更靠外,也就更好,因此也可以作为选择算法选择超参数的一种指标。其实就是PR曲线下的面积,来衡量模型的优劣,但是一般情况下都会使用另外一种曲线下的面积。由曲线的下的面积,引出下一个知识点ROC曲线。
ROC曲线(Receiver Operation Characteristic
Curve),描述TPR和FPR之间的关系。接受者操作特性曲线是指在特定刺激条件下,以被试在不同判断标准下所得的虚报概率P(y/N)为横坐标,以击中概率P(y/SN)为纵坐标,画得的各点的连线。
编程实现:
import numpy as np from sklearn import datasets from sklearn.model_selection import train_test_split import matplotlib.pyplot as plt 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 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 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 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) 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 log_reg_predict = log_reg.predict(x_test) decision_scores = log_reg.decision_function(x_test) fprs = [] tprs = [] thresholds = np.arange(np.min(decision_scores), np.max(decision_scores)) 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()
sklearn中ROC曲线的实现:
from sklearn.metrics import roc_curve fprs, tprs, thresholds = roc_curve(y_test, decision_scores) plt.plot(fprs, tprs) plt.show()
ROC曲线随着fpr的增大,tpr也在增大,通常更加关注的是曲线下的面积,如何计算曲线下的面积呢?
from sklearn.metrics import roc_auc_score # area under curve roc_auc_score(y_test, decision_scores) # 0.9830452674897119
曲线下的面积越大,说明模型的分类效果越好,这是因为在ROC曲线刚开始,fpr较低(预测为1的错误越低)的时候,tpr越大(预测为1正确的越多),曲线下的面积越大,分类算法的模型也就更好。 由输出结果可以发现ROC的AUC值对不均衡样本不是那么敏感,因此对于极度有偏的数据集查看模型的精准率和召回率曲线还是很有必要的,ROC的AUC的主要应用是比较模型或者算法的优劣。
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 x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.8, 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) y_predict = log_reg.predict(x_test) from sklearn.metrics import precision_score precision_score(y_test, y_predict, average='micro') # 可以尝试一下precision_score(y_test, y_predict),默认情况下是不支持多分类准确率预测的,但是可以通过传入超参数解决 # 0.93115438108484 from sklearn.metrics import confusion_matrix confusion_matrix(y_test, y_predict)
输出结果:
array([[147, 0, 1, 0, 0, 1, 0, 0, 0, 0], [ 0, 123, 1, 2, 0, 0, 0, 3, 4, 10], [ 0, 0, 134, 1, 0, 0, 0, 0, 1, 0], [ 0, 0, 0, 138, 0, 5, 0, 1, 5, 0], [ 2, 5, 0, 0, 139, 0, 0, 3, 0, 1], [ 1, 3, 1, 0, 0, 146, 0, 0, 1, 0], [ 0, 2, 0, 0, 0, 1, 131, 0, 2, 0], [ 0, 0, 0, 1, 0, 0, 0, 132, 1, 2], [ 1, 9, 2, 3, 2, 4, 0, 0, 115, 4], [ 0, 1, 0, 5, 0, 3, 0, 2, 2, 134]], dtype=int64)
这样看上去并不直观,绘制一下混淆矩阵。
import matplotlib.pyplot as plt 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) err_matrix
输出结果:
array([[0. , 0. , 0.00735294, 0. , 0. , 0.00657895, 0. , 0. , 0. , 0. ], [0. , 0. , 0.00735294, 0.01342282, 0. , 0. , 0. , 0.02205882, 0.02857143, 0.06802721], [0. , 0. , 0. , 0.00671141, 0. , 0. , 0. , 0. , 0.00714286, 0. ], [0. , 0. , 0. , 0. , 0. , 0.03289474, 0. , 0.00735294, 0.03571429, 0. ], [0.01342282, 0.03496503, 0. , 0. , 0. , 0. , 0. , 0.02205882, 0. , 0.00680272], [0.00671141, 0.02097902, 0.00735294, 0. , 0. , 0. , 0. , 0. , 0.00714286, 0. ], [0. , 0.01398601, 0. , 0. , 0. , 0.00657895, 0. , 0. , 0.01428571, 0. ], [0. , 0. , 0. , 0.00671141, 0. , 0. , 0. , 0. , 0.00714286, 0.01360544], [0.00671141, 0.06293706, 0.01470588, 0.02013423, 0.01333333, 0.02631579, 0. , 0. , 0. , 0.02721088], [0. , 0.00699301, 0. , 0.03355705, 0. , 0.01973684, 0. , 0.01470588, 0.01428571, 0. ]])
这个输出就是预测错误部分,整体看上去还是挺费劲的,然后绘制一下这个矩阵。
plt.matshow(err_matrix, cmap=plt.cm.gray) plt.show()
这个图中整体来说就是越亮的部分就是预测错误越多的地方,比如真值为1却预测成了9,比如真值为8预测成了1,这样就能整体看出犯错的地方在哪里,更加重要的是还能看到犯错的主要原因是什么,比如这个手写数字的识别问题其实就在于数字8和数字1的预测,容易混淆1和9,1和8,其实可以通过调整这些个二分类的阈值来提升多分类任务的准确率,这个微调的过程还是有一定的难度的。通过这样一个混淆矩阵的可视化,进一步分析出问题所在,进而对分类算法进行改进。
其实,一直都在讨论的是如何从算法层面去解决问题,做出改进,但是在机器学习这个领域,很有可能问题并不是出在算法层面,而是有可能处在样本数据层面上,比如数据集的层面上去研究一下数字1、8、9等,从数据的角度去理解为什么机器学习算法或者模型预测错误的原因,很有可能能够总结出新的特征。这也就是特征工程。总之,数据是机器学习的基础,如果没有一个好的数据还谈什么训练模型。对于数据的清理和处理是很关键的!