模型评估与选择(实战)
实战部分将结合着 理论部分 进行,旨在帮助理解和强化实操(以下代码将基于 jupyter notebook 进行)。
一、准备工作(设置 jupyter notebook 中的字体大小样式等)
import numpy as np
import os
%matplotlib inline
import matplotlib
import matplotlib.pyplot as plt
plt.rcParams['axes.labelsize'] = 14
plt.rcParams['xtick.labelsize'] = 12
plt.rcParams['ytick.labelsize'] = 12
import warnings
warnings.filterwarnings('ignore')
np.random.seed(42)
二、数据集读取与查看
Mnist 数据是图像数据:(28,28,1) 的灰度图。
Mnist 数据集有 70000 张规格较小的手写数字图片,由美国的高中生和美国人口调查局的职员手写而成。下面的代码展示了如何加载本地 Mnist 数据集( 如何导入本地Mnist 数据集):
import scipy.io
mnist = scipy.io.loadmat('resources/mnist-original.mat')
# 查看 mnist 数据集的各属性
mnist
Out
{'__header__': b'MATLAB 5.0 MAT-file Platform: posix, Created on: Sun Mar 30 03:19:02 2014',
'__version__': '1.0',
'__globals__': [],
'mldata_descr_ordering': array([[array(['label'], dtype='<U5'), array(['data'], dtype='<U4')]],
dtype=object),
'data': 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]], dtype=uint8),
'label': array([[0., 0., 0., ..., 9., 9., 9.]])}
# 查看 Mnist 数据集的内容
X = mnist['data']
print("X的内容:\n",X)
print("X的规格:",X.shape)
print()
# Mnist数据集的标签
y = mnist['label']
print("y的内容:\n",y)
print("y的规格:",y.shape)
Out
X的内容:
[[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]]
X的规格: (784, 70000)
y的内容:
[[0. 0. 0. ... 9. 9. 9.]]
y的规格: (1, 70000)
可以看出:
X 是 Mnist 数据矩阵,其中每条列向量表示每个手写数字样本的列主排序像素值矩阵(长度为28×28=784),共计70000个样本。
Y 是一条行向量,其每个值对应于 X 矩阵中每条列向量(手写数字)的真实值(标签)。
# 为了方便理解和后续使用,将 X 和 y 进行转置
X = X.T
y = y.T
# 查看执行转置后的两个矩阵
print(X.shape)
print(y[:10])
print(y.shape)
Out
(70000, 784)
[[0.]
[0.]
[0.]
[0.]
[0.]
[0.]
[0.]
[0.]
[0.]
[0.]]
(70000, 1)
# 为了后续处理数据的方便,对 y 进行拉平
y = y.flatten()
print(y[:10])
print(y.shape)
Out
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
(70000,)
对于该数据集中的某个数字,可通过将其的特征向量 reshape 为28*28的数组,然后使用 matplotlib 的 imshow 函数展示出来:
%matplotlib inline
import matplotlib
import matplotlib.pyplot as plt
# 从数据集中任意找一个数字
index = 36000
certain_digit = X[index]
# 查看其真实标签
print("该数字为:%d"%y[index])
# 将样本转为28大小的像素矩阵
certain_digit = certain_digit.reshape(28, 28)
# 按‘0’‘1’数值转为灰度图像
# interpolation当小图像放大时,interpolation ='nearest'效果很好,否则用None。
plt.imshow(certain_digit, cmap = matplotlib.cm.binary, interpolation="nearest")
plt.axis("off")
plt.show()
三、交叉验证实验
为了设计实验来对学习器的泛化误差进行评估,并对各指标进行测试,在此设计二分类实验。由于 Mnist 数据集含有数字 0-9 (具有多类),一种简单直接的设计模式是:选择某个数字(如 5 )作为正例,则其余数字即为反例。因此,这样就构建好了一个二分类实验。
1、划分数据集并置乱
# 构建测试数据和训练数据(60000份训练数据、10000份测试数据)
X_train = X[:60000]
X_test = X[60000:]
y_train = y[:60000]
y_test = y[60000:]
# 置乱操作(尽可能让各数字样本置乱,使得分布尽可能随机,也就是理论部分中提到的 “分层采样”)
import numpy as np
# 定义一个随机的索引序列
shuffle_index = np.random.permutation(60000)
# 利用随机的索引序列来重新装填训练集的内容
X_train, y_train = X_train[shuffle_index], y_train[shuffle_index]
# 查看置乱后的索引序列
shuffle_index
Out
array([12628, 37730, 39991, ..., 860, 15795, 56422])
2、设计交叉验证实验
测试集很宝贵,最好是在调参、优化之后,最终再用于测试。就像历年高考真题一样,最好是用在最后临考前进行测试,以评估自己大概能取得怎样的成绩。所以对于训练集而言,需要单独划分一些数据出来用于提前“测试”(可以理解为月考),来帮助用于调整、优化自己。
为便于实验,训练一个二分类器
因此将样本划分为:“是数字 5” 和 “非数字 5”,即二分类任务
# 构建新的标签
y_train_5 = (y_train==5)
y_test_5 = (y_test==5)
# 查看
y_train_5[:10]
Out
array([False, False, False, False, False, False, False, False, False,
True])
3、进行训练(采用随机梯度下降分类器)
from sklearn.linear_model import SGDClassifier
# 设置最大迭代次数 max_iter = 10
# 设置随机种子 random_state = 42
sgd_clf = SGDClassifier(max_iter=10, random_state = 42)
# 开始训练
sgd_clf.fit(X_train,y_train_5)
4、用训练好的分类器进行预测
print(y[index])
print(sgd_clf.predict([X[index]]))
Out
5.0
[ True]
5、使用交叉验证测量准确性
① 直接调用方法(一步到位)
from sklearn.model_selection import cross_val_score
# 该方法会自动对数据集进行切分并对切分后的样本进行交叉检验
# cv 表示交叉验证的次数(每次都将数据划分为 cv 份,并采取“留一法”进行验证)
# scoring 选择 "accuracy" 表示用准确率来衡量
cross_val_score(sgd_clf,X_train,y_train_5,cv=3,scoring='accuracy')
Out
array([0.96845, 0.9591 , 0.9521 ])
② 自定义交叉验证过程
# 调用切分数据方法
from sklearn.model_selection import StratifiedKFold
# 为了在每次训练时,使用一个规格一致,而都是未经训练的分类器,这里调用了 clone 库
from sklearn.base import clone
# 将数据集切分 n_splits 份,并进行 n_splits 次交叉实验(每次都采取“留一法”单独用一组数据来验证,并计算得分)
skflods = StratifiedKFold(n_splits=3,random_state=42,shuffle=True)
i = 0;
# 对每份数据重新进行 fit 操作
for train_index,test_index in skflods.split(X_train,y_train_5):
# 克隆分类器
clone_clf = clone(sgd_clf);
# 划分数据集
X_train_folds = X_train[train_index]
y_train_folds = y_train_5[train_index]
X_test_folds = X_train[test_index]
y_test_folds = y_train_5[test_index]
# 开始训练
clone_clf.fit(X_train_folds,y_train_folds)
# 进行预测
y_pred = clone_clf.predict(X_test_folds)
# 对预测结果进行评估
i += 1
cnt_correct = sum(y_pred==y_test_folds)
# 但是这里执行 y_pred==y_test_folds 得到的是一个二维元组
print("第 %d 次评估的准确率为 %.2f%% "%(i,cnt_correct/len(y_pred)*100))
Out
第 1 次评估的准确率为 96.80%
第 2 次评估的准确率为 95.82%
第 3 次评估的准确率为 94.80%
说明
StratifiedKFold 类实现了分层采样,生成的折(fold)包含了各类相应比例的样例。在每一次迭代,上述代码生成分类器的一个克隆版本,在训练折(training folds)的克隆版本上进行训,在测试折(test folds)上进行预测。然后它计算出被正确预测的数目和输出正确预测的比例。
上述实验能输出大于 95% 的精度(accuracy),还是算比较高的。但需要注意的是,这是一个有数据偏差的数据集,这是因为只有 10% 的图片是数字 5。因此,若你总是猜测某张图片不是 5,你也会有90%的可能性是对的。这就说明,精度这个评估指标并不总是很精确,故考虑其他的一些评估模型。
四、混淆矩阵实验
对分类器来说,一个好得多的性能评估指标是混淆矩阵。为了计算混淆矩阵,首先需要有一系列的预测值,这样才能将预测值与真实值做比较。或许你想在测试集上做预测,但是请记住,测试集相当珍贵,现在先不要碰它。记住,只有当处于项目尾声时(即准备上线一个分类器的时候),才应该使用测试集。因此,需要用到 cross_val_predict() 函数:
from sklearn.model_selection import cross_val_predict
y_train_pred = cross_val_predict(sgd_clf,X_train,y_train_5,cv=3)
print(X_train.shape)
print(y_train_pred.shape)
print(y_train_pred[:10])
Out
(60000, 784)
(60000,)
[False False False False False False False False False False]
和 cross_val_score()方法类似,cross_val_predict()也使用 K 折交叉验证。但它不是返回一个评估分数,而是返回基于每一个测试折做出的一个预测值。这意味着,对于每一个训练集的样例,你得到的预测是较为干净的(“干净”是说一个模型在训练过程中没有用到测试集的数据)。
# 可通过调用 confusion_matrix( ) 方法计算混淆矩阵
from sklearn.metrics import confusion_matrix
confusion_matrix(y_train_5,y_train_pred)
Out
array([[53334, 1245],
[ 1162, 4259]], dtype=int64)
对该输出结果进行解释
negative class[[true negative , false positive],
positive class [false negative , true positive]]
true negative:53834个“非5”数据被正确地分类到“非5”类别中;
false positive:1245个“非5”数据被错误地分类到“5”类别中;
false negative:1162个“5”数据被错误地分类到“非5”类别中;
true positive:4259个“5”数据被正确地分类到“5”类别中;
一个完美的分类器应该只有 true positives 和 true negatives,即主对角线元素不为0,其余元素为0。
五、查准率、查全率与F1 度量
查准率也称精度,precision=TP/(TP+FP)
查全率也称召回率,recall=TP/(TP+FN)
在实际使用时,可以通过调用现成的包来完成该工作。
from sklearn.metrics import precision_score,recall_score
print("查准率为 %.2f%%" % (precision_score(y_train_5,y_train_pred)*100) )
print("查全率为 %.2f%%" % (recall_score(y_train_5,y_train_pred)*100) )
Out
查准率为 77.38%
查全率为 78.56%
将 Precision 和 Recall 综合考虑,得到一个称为 F1 score 的指标,其定义如下:
F1 = 2/(1/P + 1/R) = 2×TP/(样例总数+TP-TN)
可见调和平均值给予低值更多权重。因此,如果召回和精确度都很高,分类器将获得高F1分数。
from sklearn.metrics import f1_score
print("F1分数为 %.2f%%" % (f1_score(y_train_5,y_train_pred)*100) )
Out
F1分数为 77.97%
说明
F1 支持那些有着相近精确率和召回率的分类器。这不会总是你想要的。有的场景你会绝大程度地关心精确率,而另外一些场景你会更关心召回率。不幸的是,你不能同时拥有两者。增加精确率会降低召回率,反之亦然。这叫做精确率与召回率之间的折衷.一般来讲,提高阈值,会有假正例成为一个真反例,从而提高准确率相反,降低阈值可提高召回率、降低精确率。
六、阈值对结果的影响
在用分类器进行预测,即调用方法 clf.predict() 时,最终返回的是一个 bool 值(指代是与否)。 但是实际上,其真正的过程如下:
1、对所有样本调用 clf.decision_function() 计算数据在通过拟合函数计算后得到的分数;
2、划定阈值;
3、根据阈值来将样本数据的预测值进行分类并反馈结果。
因此,当划分阈值不同,最终得到的分类结果也是不同的。
print(sgd_clf.decision_function([X[35000]]))
Out
[72642.95869024]
decision_function() 方法
Scikit-Learn 不让你直接设置阈值,但是它给你提供了设置决策分数的方法,这个决策分数可以用来产生预测。它不是调用分类器的predict()方法,而是调用decision_function()方法。这个方法返回每一个样例的分数值,然后基于这个分数值,使用你想要的任何阈值做出预测。
y_scores = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3, method="decision_function")
print(y_scores[:20])
Out
[ -224681.3341746 -1221050.0034231 -400939.32983044 -266651.26427417
-280607.93249529 -135657.99496428 -363362.95183448 -165326.04899933
-127209.12102694 -94402.75492628 -158981.3199601 -186616.55097065
-74914.12777259 -188881.59402562 -276974.73141498 -509168.5523472
-120346.48545926 -470189.0536452 50253.31653524 178951.66740394]
现在有了这些分数值。对于任何可能的阈值,使用precision_recall_curve(),都可以计算精确率和召回率:
from sklearn.metrics import precision_recall_curve
precisions, recalls, thresholds = precision_recall_curve(y_train_5, y_scores)
print("阈值的个数:%d"%thresholds.shape)
print("前20个阈值:")
print(thresholds[:20])
print()
print("精度的个数:%d"%precisions.shape)
print("前20个精度:")
print(precisions[:20])
print()
print("召回率的个数:%d"%recalls.shape)
print("前20个召回率:")
print(recalls[:20])
Out
阈值的个数:60000
前20个阈值:
[-1670416.9594504 -1502649.89986813 -1416789.74342245 -1412150.72156594
-1405576.24912445 -1397379.17132625 -1385575.1825487 -1384407.94197157
-1384080.85886164 -1375785.00276224 -1370136.93381444 -1367408.93916748
-1363398.02001214 -1358099.29781036 -1332582.05787757 -1325851.61715328
-1322069.34979195 -1319612.9424828 -1318399.76458103 -1304716.41374972]
精度的个数:60001
前20个精度:
[0.09035 0.09035151 0.09035301 0.09035452 0.09035602 0.09035753
0.09035904 0.09036054 0.09036205 0.09036355 0.09036506 0.09036657
0.09036807 0.09036958 0.09037109 0.09037259 0.0903741 0.09037561
0.09037711 0.09037862]
召回率的个数:60001
前20个召回率:
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
# 绘制图像
def plot_precision_recall_vs_threshold(precisions, recalls, thresholds):
plt.plot(thresholds, precisions[:-1], "b--", label="Precision")
plt.plot(thresholds, recalls[:-1], "g-", label="Recall")
plt.xlabel("Threshold")
plt.legend(loc="upper left")
plt.ylim([0, 1])
plt.xlim([-600000, 600000])
plot_precision_recall_vs_threshold(precisions, recalls, thresholds)
plt.show()
说明
当提高阈值时,精确率通常会随之提高,但这并不绝对;而当提高阈值时,召回率只会降低。这就解释了为什么召回率的曲线更加平滑,而精确率的曲线有时会起伏不平。
根据上面得到的图像,可以选择出适合你任务的最佳阈值。
七、P-R Curve
另一个选出好的 精确率/召回率 折中的方法是直接画出 精确率对召回率 的曲线(PR曲线)。在很多情形下我们可根据学习器的预测结果对样例进行排序,排在前面的是学习器认为“最可能”是正例的样本。排在最后的则是学习器认为“最不可能”是正例的样本。按此顺序逐个把样本作为正例进行预测,则每次可以计算出当前的查全率、查准率。以查准率为纵轴、查全率为横轴作图,就得到了查准率-查全率曲线,简称 P-R 曲线"显示该曲线的图称为 P-R 图"。
plt.plot(precisions, recalls)
plt.xlabel("Recall")
plt.ylabel("Precision")
plt.show()
八、ROC Curve
全称 Receiver Operating Characteristic Curve(受试者特征曲线)。
它与 P-R曲线 非常相似,但不是画出准确率对召回率的曲线。ROC 曲线是真正例率(true positive rate,TPR)对假正例率(false positive rate, FPR)的曲线。其中:
TPR = TP/(TP+FN),其实就是召回率
FPR = FP/(FP+TN)
FPR 是反例被错误分成正例的比率。它等于 1 减去真反例率(true negative rate,TNR)。TNR是反例被正确分类的比率。TNR也叫做特异性。所以 ROC 曲线画出召回率对(1 减特异性)的曲线。
TNR=TN/(TN+FP)
FPR = 1-TNR
from sklearn. metrics import roc_curve
fpr, tpr, thresholds = roc_curve(y_train_5, y_scores)
def plot_roc_curve(fpr, tpr, label=None):
plt.plot(fpr, tpr, linewidth=2, label=label)
plt.plot([0, 1], [0, 1], 'k--')
plt.axis([0, 1, 0, 1])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plot_roc_curve(fpr, tpr)
plt.show()
比较分类器优劣的一个方法是:测量ROC曲线下的面积(AUC)。
完美分类器的 ROC 曲线下面积等于 1(或等于0),而一个纯随机分类器 ROC 曲线下的面积等于 0.5。Scikit-Learn 提供了一个函数来计算AUC:
from sklearn.metrics import roc_auc_score
roc_auc_score(y_train_5, y_scores)
Out
0.9597417516899552
选用两类曲线的准则
PRC 和 ROC很相似,那如何决定在什么情况下该优先使用何种曲线呢?一个笨拙的规则是:当正例很少,或当你关注假正例多于假反例的时候优先使用 PRC;其他情况则使用 ROC 曲线。举个例子,上面的 ROC 曲线和 AUC 值都取得较为不错的效果,你或许认为这个分类器很棒。但出现这情况几乎全是因为只有少数正例(“是 5”),而大部分是反例(“非 5”);相反,PR 曲线清楚显示出这个分类器还有很大的改善空间(PR 曲线应该尽可能地靠近右上角)。
总结
PR和ROC在面对不平衡数据时的表现是不同的。在数据不平衡时,PR曲线是敏感的,随着正负样本比例的变化,PR会发生强烈的变化。而ROC曲线是不敏感的,其曲线能够基本保持不变。ROC 对数据的不敏感表明其在衡量一个模型本身的预测能力时,与样本正负比例无关。但是这个不敏感的特性又使得其较难以看出一个模型在面临样本比例变化时模型的预测情况。而PRC因为对样本比例敏感,因此能够看出分类器随着样本比例变化的效果。而实际中的数据又是不平衡的,这样有助于了解分类器实际的效果和作用,也能够以此进行模型的改进。
综上,在实际学习中,我们可以使用ROC来判断两个分类器的优良,然后进行分类器的选择,然后可以根据PRC表现出来的结果衡量一个分类器面对不平衡数据进行分类时的能力,从而进行模型的改进和优化。
END
转载:https://blog.csdn.net/the_ZED/article/details/128458861