一.Python代码
#!/usr/bin/env python3
# encoding: utf-8
'''
@file: rock_mine_performance_measurement.py
@time: 2020/5/30 0030 16:14
@author: Jack
@contact: jack18588951684@163.com
'''
import urllib.request
import numpy
from sklearn import datasets, linear_model
from sklearn.metrics import roc_curve, auc
import pylab as pl
def confusionMatrix(predicted, actual, threshold):
if len(predicted) != len(actual):
return -1
tp = 0.0
fp = 0.0
tn = 0.0
fn = 0.0
for i in range(len(actual)):
if actual[i] > 0.5:
if predicted[i] > threshold:
tp += 1.0
else:
fn += 1.0
else:
if predicted[i] < threshold:
tn += 1.0
else:
fp += 1.0
rtn = [tp, fn, fp, tn]
return rtn
target_url = (
"https://archive.ics.uci.edu/ml/machine-learning-databases/undocumented/connectionist-bench/sonar/sonar.all-data")
data = urllib.request.urlopen(target_url)
xList = []
labels = []
for line in data:
## 按逗号切
row = str(line, encoding='utf-8').strip().split(',')
if row[-1] == 'M':
labels.append(1.0)
else:
labels.append(0.0)
row.pop()
floatRow = [float(num) for num in row]
xList.append(floatRow)
# 将属性矩阵和label分成训练集training set(2/3 of data)和测试集test sets (1/3 of data)
indices = range(len(xList))
xListTest = [xList[i] for i in indices if i % 3 == 0]
xListTrain = [xList[i] for i in indices if i % 3 != 0]
labelsTest = [labels[i] for i in indices if i % 3 == 0]
labelsTrain = [labels[i] for i in indices if i % 3 != 0]
xTrain = numpy.array(xListTrain)
yTrain = numpy.array(labelsTrain)
xTest = numpy.array(xListTest)
yTest = numpy.array(labelsTest)
print("Shape of xTrain array", xTrain.shape)
print("Shape of yTrain array", yTrain.shape)
print("Shape of xTest array", xTest.shape)
print("Shape of yTest array", yTest.shape)
## 训练线性回归模型
rocksVMinesModel = linear_model.LinearRegression()
rocksVMinesModel.fit(xTrain, yTrain)
## 生成训练集预测值
trainingPredictions = rocksVMinesModel.predict(xTrain)
print("一些模型预测值:")
print(trainingPredictions[0:5])
print(trainingPredictions[-6:-1])
## 生成训练集上的混淆矩阵
confusionMatTrain = confusionMatrix(trainingPredictions, yTrain, 0.5)
tp = confusionMatTrain[0]
fn = confusionMatTrain[1]
fp = confusionMatTrain[2]
tn = confusionMatTrain[3]
print("tp = " + str(tp) + "\tfn = " + str(fn) + "\n" + "fp = " +
str(fp) + "\ttn = " + str(tn) + '\n')
## 生成测试集预测值
testPredictions = rocksVMinesModel.predict(xTest)
## 生成测试集上的混淆矩阵
confusionMatTest = confusionMatrix(testPredictions, yTest, 0.5)
tp = confusionMatTest[0]
fn = confusionMatTest[1]
fp = confusionMatTest[2]
tn = confusionMatTest[3]
print("tp = " + str(tp) + "\tfn = " + str(fn) + "\n" + "fp = " +
str(fp) + "\ttn = " + str(tn) + '\n')
## 生训练集ROC曲线
fpr, tpr, thresholds = roc_curve(yTrain, trainingPredictions)
roc_auc = auc(fpr, tpr)
print('AUC for in-sample ROC curve:{}'.format(roc_auc))
## 绘制ROC曲线
pl.clf()
pl.plot(fpr, tpr, label='ROC curve (area = %0.2f)' % roc_auc)
pl.plot([0, 1], [0, 1], 'k-')
pl.xlim([0.0, 1.0])
pl.ylim([0.0, 1.0])
pl.xlabel('False Positive Rate')
pl.ylabel('True Positive Rate')
pl.title('In sample ROC rocks vs mines')
pl.legend(loc='lower right')
pl.show()
## 生成测试集ROC曲线
fpr, tpr, thresholds = roc_curve(yTest, testPredictions)
roc_auc = auc(fpr, tpr)
print('AUC for out-of-sample ROC curve: {}'.format(roc_auc))
## 绘制ROC曲线
pl.clf()
pl.plot(fpr, tpr, label='ROC curve (area = %0.2f)' % roc_auc)
pl.plot([0, 1], [0, 1], 'k-')
pl.xlim([0.0, 1.0])
pl.ylim([0.0, 1.0])
pl.xlabel('False Positive Rate')
pl.ylabel('True Positive Rate')
pl.title('Out-of-sample ROC rocks vs mines')
pl.legend(loc="lower right")
pl.show()
Shape of xTrain array (138, 60)
Shape of yTrain array (138,)
Shape of xTest array (70, 60)
Shape of yTest array (70,)
一些模型预测值:
[-0.10240253 0.42090698 0.38593034 0.36094537 0.31520494]
[1.11094176 1.12242751 0.77626699 1.02016858 0.66338081]
tp = 68.0 fn = 6.0
fp = 7.0 tn = 57.0
tp = 28.0 fn = 9.0
fp = 9.0 tn = 24.0
AUC for in-sample ROC curve:0.9795185810810811
AUC for out-of-sample ROC curve: 0.8484848484848485
二.性能分析
代码第一部分将数据集读入并将其解析为包含标签与属性的记录。下一步将数据划分为 2 个子集:测试集包含 1/3 的数据,训练集包含剩下 2/3 的数据。标注为 test 的数据集不能用于训练分类器,但会被保留用于评估训练得到的分类器性能。这一步用来模拟分类器在新数据样本上的行为。分类器的训练通过将标签 M(代表水雷)以及标签 R(代表岩石)转换为 2 个数值:1.0 对应于水雷,0.0 对应于岩石,然后使用最小二乘法来拟合一个线性模型。上面方法理解实现起来都非常简单,性能也接近于更加复杂的方法。使用 scikit-learn 中的线性回归包来训练普通的最小均方模型。训练的模型用于在训练集和测试上生成预测结果。打印一些预测值的样例。线性回归模型产生的预测大部分集中在 0.0 到 1.0,然而也并非全部。这些预测不只是概率。仍然可以将它们与决策阈值进行比较来生成分类标签。函数 confusionMatrix() 生成了混淆矩阵,该函数以预测值、对应的实际标签以及决策阈值作为输入,函数将预测值与决策阈值进行比较来决定为每个样本赋“正值”或者“负值”,预测值对应于混淆矩阵中的列。函数再根据样本的实际标签将样本放在下表中对应位置:
每个决策阈值的错误率都可以从混淆矩阵中计算得到。总的错误数为FP与FN的加和。代码分别在训练集以及测试集上计算混淆矩阵,并且打印出来。在训练集上误分类率为 8%,在测试集上误分类率为 26%。一般来讲,测试集上的性能要差于训练集上的性能。在测试集上的结果更能代表错误率。当决策阈值改变的话,误分类率也会改变。表3-2显示随着决策阈值的变化,误分类率的变化情况:
最佳决策阈值应该是能够最小化误分类率的值。然而,不同类错误对应的代价可能是不同的。例如,对于岩石-水雷预测问题,如果将岩石预测为水雷,可能会花费 $100 请潜水员下水确认;如果将水雷预测为岩石,那么未爆炸的水雷不移除的话可能会导致 $1,000美元的人身财产损失。一个FP的样本代价为 100,一个 FN 的样本代价为1,000。有了这样的假设,不同决策阈值生成的错误代价如表 3-3 所示。将水雷误分类为岩石(不对其进行处理可能会威胁到健康安全)的高代价会使最优决策阈值趋向于0。这意味着会产生更多FP,因为FP的代价不高。一项完整的分析应该包括移除水雷的代价以及随着移除带来的 1,000 美元的好处。如果这些数值已经知道(或者接近于合理近似),它们理应在计算阈值时考虑。
注意到总的 FP 以及 FN 的相对代价取决于数据集中正例与负例的比例。岩石-水雷数据集有相同数量的正例与负例(岩石和水雷)。这些都是实验中的常用假设。实际遇到的正例数和负例数可能完全不同。在系统部署场景下,如果正负例数目不同,就可能要基于实际应用比例做一些调整。
数据科学家可能并没有关于正负样本误分的具体代价值,但仍然想使用除了误分类率外的其他刻画错误的方法。一种常见的指标被称作接收者操作曲线或者 ROC 曲线。
ROC从其初始应用中继承了对应的名字:通过处理雷达信号来判断是否有敌机出现。ROC曲线使用一个图来展示不同的列联表,图中绘制的是真正率(简写为 TPR)随假正率(FPR)的变化情况。TPR 代表被正确分类的正样本比例(参见公式 3-8)。FPR 是 FP 相对于实际负样本的比例(参见公式 3-9)。从列联表的元素来看,这些值通过下面的公式计算得到。
简单想一下,如果决策阈值使用非常小的值,那么每个样本都会被预测为正例。此时,FN=0.0(因为每个样本都被分类为正例,没有假负例),TN=0.0(没有例子被分类为负例),所以TPR=1.0,FPR=1.0。然而当决策阈值设得很高,TP=0,FP=0(因为没有样本被分类为正例),那么 TPR=0,FPR=0。
如果分类器(针对岩石-水雷问题)为随机分类,ROC 的样子为一条从左下角到右上角的对角线。这条对角线一般画在图里作为参照点。对于一个完美的分类器,ROC 曲线应该是直接从(0,0)上升到(0,1),然后横着连到(1,1)的直线。分类器越接近于左上角,效果越好。如果 ROC 曲线掉到对角线下边,这一般表示预测符号弄反了,此时应该认真检查代码。
AUC指的是ROC曲线下的面积。一个完美分类器的 AUC=1.0,随机猜测分类器对应的AUC为0.5。基于训练集进行错误估计往往会高估性能。训练集上的AUC=0.98,测试集上的AUC=0.85。一些用于估算二分类问题性能的方法同样适用于多分类问题。误分类错误仍然有意义,混淆矩阵也同样适用。
三.数据预留方法
上述例子将数据切分为 2 个子集:第一个子集称作训练集,包含 2/3 的可用数据,用于拟合一个普通的最小均方模型;第 2个子集包含剩下 1/3 的数据,称作测试集,用于评估性能(不在模型训练中使用)。对于机器学习,以上步骤是一个标准流程。尽管目前没有明确规则来确定测试集的大小,一般测试集可以占所有数据的25% ~ 35%。模型训练的性能随着训练集规模的减小而下降,将过多数据从训练集中去掉会影响模型性能估计。
除了上述的数据预留方法之外,还有一种被称作“n折交叉验证”的数据预留方法。下图展示了如何基于 n 折交叉验证来对数据进行切分。数据集被等分为 n 份不相交的子集。训练和测试需要多次遍历数据。图 中的 n=5。第一次遍历是,数据的第一块被预留用于测试,剩下 n-1 块用于训练。第 2 遍,第 2 块被预留做测试,剩下 n-1 块用于训练。该过程继续,直到所有数据都被预留一遍(对于图中5 折交叉验证的样例中,数据会被扫描 5 次)。
n 折交叉验证可以估计预测错误:在多份样本上估计错误来估计错误边界。通过为训练集分配更多样本,生成的模型会产生更低的泛化错误,具备更好的预测性能。例如,如果选择 10 折交叉验证,每次训练只需要留出 10% 的数据进行预测。n 折交叉验证是以更多的训练时间作为代价。保留一个固定的集合作为测试集可以有更快的训练速度,因为它只需要扫描一遍训练数据。当使用 n 折交叉验证的训练时间不可忍受时,使用预留测试集是一个更好的选择,而且如果训练数据很多的话,留一些数据出来不会对模型性能造成太大影响。
另外需要特别注意的是测试样本应该能代表整个数据集。上述例子使用的抽样样本不完全是一个随机样本(每隔 3 个样本选 1 个作为测试样本)。但是对稀疏事件(如欺诈或者广告点击)进行预测时,要建模的事件出现频率非常少,随机抽样可能导致有过多或者过少的样本出现在测试集中,同时导致对性能的错误估计。此时就需要用到分层抽样将数据切分为不同子集,分别在子集中进行抽样然后组合。如果类别标签对应罕见事件,可能需要分别从欺诈样本以及合法样本中抽样,然后组成测试集。更重要的是,这样的数据才是模型最终要运行的数据。
模型经过训练和测试,最后还应该将训练集和测试集再合并为一个更大的集合,重新在该集合(包含训练集+测试集)上训练模型。测试集已经可以给出预测错误的期望结果,即已经完成了模型性能评估的任务(预留一部分数据的原因)。如果能在更多数据上进行训练,模型效果会更好,泛化能力也更好。因而,真正要部署的模型应该在所有数据上进行训练。