目录
上次实验我们已经实现了knn算法,这次实验我们来对分类算法进行性能评估。
上次实验我们对knn分类器的性能评估只有错误率这一个单一的指标,对其的评估较为片面,这次实验我们将使用精确率Precision、召回率Recall 等指标以及ROC曲线和PR曲线对分类器进行更加全面的评估。
一、选择分类器进行训练与测试
1. 选择数据集
这次实验我们要进行评估的是二分类的模型,即样本标签只有两种:0和1,将0视为负样本,将1视为正样本。因此我们要选择二分类的数据集。
我们可以选择sklearn自带的小数据集:乳腺癌数据集load-barest-cancer(),也可以使用sklearn库中生成数据集的方法make_classification() 来自定义生成一个数据集。
这里我使用make_classification()函数来生成了一个二分类的数据集
对于make_classification()函数,一般我们使用到的参数有:
n_samples:生成的样本数。
n_features:生成的特征数。
n_classes:生成的样本标签数(缺省时为2)random_state:生成数据集的随机数(相同的随机数生成的数据集是一样的)
这里我使用2的随机数生成了一个二分类数据集,其含有1000个样本,每个样本有10个特征
X, y = sklearn.datasets.make_classification(n_samples=1000, n_features=10, random_state=2)
打印数据集部分数据
2. 对测试集进行分类
在计算模型的真正率和假正率时,我们希望能得到多组的值,这样才能对模型进行全面的评估。
我们可以将分类得到的值从离散值改为连续值,这个值位于0到1之间,它的含义是样本为正样本的概率或置信度,通过更改阈值来调整正样本和负样本的数量以计算出多组真正率和假正率的值。
这里我们还是使用knn算法进行分类,不过需要对knn算法进行一些改变,将结果改为预测为正样本的概率,我们可以用该样本k个近邻样本中正样本的个数除以k的值来表示该样本的预测值。
我们只需更改分类函数classify2,将其返回值改为k个近邻样本中正样本的个数。
def classify2(inX, data_matrix, labels, k):
sq_dis = (np.tile(inX, (len(data_matrix), 1)) - data_matrix) ** 2
dis_mat = np.sum(sq_dis, axis=1) ** 0.5
near_type = {}
for i in range(k):
if(labels[np.argsort(dis_mat)[i]] not in near_type.keys()):
near_type[labels[np.argsort(dis_mat)[i]]] = 1
else:
near_type[labels[np.argsort(dis_mat)[i]]] += 1
# 从near_type字典中取键值对,对键为1(标签为1)的值进行累加,
# 得到的就是k个近邻样本中正样本的个数
return sum(value for key, value in near_type.items() if key == 1) / k, near_type
部分预测输出:
我们也可以直接使用sklearn库中的已实现的knn进行分类,再使用predict_proba()函数得到使用该分类算法每个样本属于各个类别的预测概率。
sklearn库中的knn分类器使用fit()函数训练需要传入的参数是训练集特征和标签,需要先使用train_test_split()函数分离训练集和测试集。
train_test_split()函数需要传入4个参数:数据集样本特征、样本标签,测试集比例、随机数种子。
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=12)
clf = sklearn.neighbors.KNeighborsClassifier(n_neighbors=5)
clf.fit(X_train, y_train)
# 每个预测结果包含两个值:样本标签为0的概率和为1的概率,我们只需要为1的概率,因此切片每行的第二列
y_hat = clf.predict_proba(X_test)[:, 1]
print('实际值:\n', y)
print('预测值:\n', y_hat)
注意使用predict_proba()函数得到的是样本属于各个类别的概率,我们只需要样本为正样本的概率,因此需要将结果切片只取第二列的数据。
输出(只显示部分):
使用库函数在将测试集和训练集分开时是打乱顺序的,而我自己实现的knn算法并没有打乱顺序,因此使用库函数实现预测结果可能会稍微好一些。
二、绘制ROC曲线
1. 计算ROC曲线数据点
ROC曲线的数据点包括TPR和FPR的值。
TPR和FPR是二分类模型中常用的性能指标,也是本次我们实验绘制roc曲线所需要的横纵坐标值,因此我们需要计算FPR和TPR的值。
作为绘制曲线的数据,FPR和TPR的值需要有多个,我们可以通过更改阈值,改变正负样本的数量,获得多个FPR和TPR的值。
在计算TPR和FPR前我们需要先计算TP、FP、TN、FN
T、F指的是true、false,即正确和错误,在这里面的意思是预测的结果是正确还是错误。
P、N指的是positive、negative,即正、负,在这里面是正样本和负样本的意思。
TP:正确预测为正的样本数量(实际为正,预测结果也为正)
FP:错误预测为正的样本数量(实际为负,预测结果却为正)
TN:正确预测为负的样本数量(实际为负,预测结果也为负)
FN:错误预测为负的样本数量(实际为正,预测结果却为负)
据此,我们可以得到计算TP、FP、TN、FN的方法
# 计算tp、tn、fp、fn
def cal_tpfn(y_test, y_hat):
tp = fp = tn = fn = 0
for y1, y2 in zip(y_test, y_hat):
if (y1, y2) == (0, 0):
tn += 1
if (y1, y2) == (0, 1):
fp += 1
if (y1, y2) == (1, 0):
fn += 1
if (y1, y2) == (1, 1):
tp += 1
return tp, fp, tn, fn
TPR即真正率,同时也是召回率,它是正确预测为正的样本数量占所有实际为正的样本数量(测试集中样本的标签即为实际标签值)的比例,即TP / (TP + FN),它衡量的是分类器算法对正样本的预测能力,这个值越高分类器性能越好。
FPR即假正率,它是错误预测为正的样本数量占所有实际为负的样本数量的比例,即FP / (FP + TN),它衡量的是分类器将负样本错误识别为正样本的能力,这个值越低分类器越不容易将负样本错误识别为正样本,显示这个值越低分类器性能越好。
我们再以预测结果中的所有概率作为阈值,再将预测结果根据阈值更改为0或1(大于阈值的为1,小于阈值的为0),然后对每个阈值进行计算TPR和FPR即可。
这样我们就可以自己实现一个简单的计算ROC曲线数据点的函数
def simple_roc_curve(y_test, y_hat):
# 去除重复项并降序排序
thresholds = sorted(set(y_hat), reverse=True)
fprs = []
tprs = []
for threshold in thresholds:
# 将大于阈值的预测概率值标为正样本(将其值设为1),否则标为负样本(将其值设为0)
y_pred = [1 if value >= threshold else 0 for value in y_hat]
tp, fp, tn, fn = cal_tpfn(y_test, y_pred)
# 计算真正率和假正率,真正率为正确预测为正样本的数量与所有正样本的数量(测试集中的正样本)
# 假正率为错误预测为正样本的数量与所有负样本的数量(测试集中的负样本)
tpr = tp / (tp + fn) if (tp + fn) != 0 else 0
fpr = fp / (fp + tn) if (fp + tn) != 0 else 0
# 将该阈值下的TPR和FPR加入到TPR和FPR集合中
fprs.append(fpr)
tprs.append(tpr)
return fprs, tprs, thresholds
注意获取阈值时需要使用set()函数去除重复的值,以防重复计算,同时需要进行排序,pyplot的画图是一个点一个点画的,如果是没有顺序的数据,那么画出来的图将会非常混乱,不是一条平滑的曲线,使用升序排序或降序排序都可以。
打印输出:
接着我们需要计算ROC曲线的AUC,AUC即曲线下面积,就是曲线与x轴y轴所包围的面积,也可以说是对曲线函数的积分。
我们很难手动计算AUC的值,因此我们使用sklearn库metrics模块中的roc_auc_score()函数来计算ROC的曲线下面积,该函数输入的参数为测试集样本的实际值和样本的预测值。
from sklearn.metrics import *
roc_auc = roc_auc_score(y_test, y_hat)
也可以使用auc()函数来计算,该函数传入的参数为一组TPR和FPR的值
roc_auc = auc(fprs, tprs)
计算结果:
2. 绘制ROC曲线
ROC Curve(Receive Operating Characteristic Curve):接受者操作特性曲线,它是用来衡量分类器性能的一种曲线,
我们可以使用matplotlib库中的pyplot来轻松绘制ROC曲线,我们只需给出一组TPR和FPR的值就可以绘制出ROC曲线
plot()函数接收横纵坐标数据进行绘图,ROC曲线横坐标的值为FPR,纵坐标的值为TPR,依次传入fprs和tprs即可进行绘图,此外可选的参数有color(曲线颜色:使用英文颜色名或者rgb值)、lw(曲线宽度)、label(曲线的标签:输入标签文本)设定标签后,使用plt.legend()函数进行显示,legend函数可以设置标签显示的位置loc,这里设置为lower right(右下角)。
如下代码传入一组FPR和TPR的值,并设定颜色为红色,曲线宽度为2,曲线标签为“ROC CURVE(auc='上面的计算值')” 后,进行画图。
# 绘制图形
plt.plot(fprs, tprs, color='red', lw=2, label='ROC CURVE (auc = %0.2f)' % roc_auc)
# 设置x轴和y轴的标签
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
# 设置图形的标题
plt.title('Receive Operating Characteristic Curve')
# 显示标签
plt.legend(loc='lower right')
得到的曲线并不理想,纵坐标的范围只有0.5到1.0,不能整体的显示曲线。
这是因为上面计算的ROC数据点中不包括(0,0)点,且没有为图形标定横纵坐标的范围。
一般来说,在计算ROC数据点时,都会加入一个大于1的值,在这个阈值下,TPR和FPR的值都会是0,使得ROC曲线经过(0,0)点。
在画图时为了显示更完整更规范的图形,我们通常需要设置图形的横纵坐标的范围,横坐标范围一般为0到1,纵坐标的范围可以稍大一些,因为在阈值较小(接近0)时,TPR会很接近1,为了显示的更清楚,通常将纵坐标的范围设为0到1.05。此外,在后面绘制多个分类器时,曲线会很接近,为了使曲线稍微分离一些,可以将图形的宽度适当拉长一些。
def simple_roc_curve(y_test, y_hat):
thresholds = sorted(set(y_hat), reverse=True)
# 添加一个大于1的值,使roc曲线包含(0,0)点
thresholds.append(2)
thresholds.sort(reverse=True)
fprs = []
tprs = []
for threshold in thresholds:
# 将大于阈值的预测概率值标为正样本(将其值设为1),否则标为负样本(将其值设为0)
y_pred = [1 if value >= threshold else 0 for value in y_hat]
tp, fp, tn, fn = cal_tpfn(y_test, y_pred)
# 计算真正率和假正率,真正率为正确预测为正样本的数量与所有正样本的数量(测试集中的正样本)
# 假正率为错误预测为正样本的数量与所有负样本的数量(测试集中的负样本)
tpr = tp / (tp + fn) if (tp + fn) != 0 else 0
fpr = fp / (fp + tn) if (fp + tn) != 0 else 0
fprs.append(fpr)
tprs.append(tpr)
return fprs, tprs, thresholds
或者我们也可以直接使用sklearn库metrics模块中的roc_curve函数,该函数中会在阈值中添加一个inf(无穷大)值。
fprs, tprs, roc_thresholds = roc_curve(y_test, y_hat)
改进后的绘图:
# 设置曲线的长宽,使图形适当拉长
plt.figure(figsize=(12, 5))
# 绘制图形
plt.plot(fprs, tprs, color='red', lw=2, label='ROC CURVE (auc = %0.2f)' % roc_auc)
# 设置x轴y轴的范围
plt.xlim([0, 1])
plt.ylim([0, 1.05])
# 设置x、y轴的标签
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
# 设置曲线的标题
plt.title('Receive Operating Characteristic Curve')
# 显示曲线标签
plt.legend(loc='lower right')
重新绘制图形:
这样绘制出来的图形就直观多了,通过ROC曲线的曲线下面积我们可以衡量分类器的性能,面积越大,性能越好。
在绘制ROC曲线时通常我们再添加一个过(0,0)和(1,1)点的对角线,用于描述随机分类器的性能,随机分类器的auc面积默认为0.5,如果分类器ROC曲线在这条线之上,则说明该分类器的性能比随机分类器好(也可以看auc的值)。
添加对角线:
plt.plot([0, 1], [0, 1], color='blue', lw=2, linestyle='--')
此分类器的ROC曲线的AUC值为0.89,是个非常不错的值,表明此算法的性能较好。
三、绘制PR曲线
1. 计算PR曲线数据点
pr曲线数据点包括Precision(精确率)和Recall(召回率)
Precision的值是正确预测为正的样本数量占所有预测为正的样本数量(包括错误预测为正和正确预测为正)的比例,即Precision = TP / (TP + FP) ,它衡量的是分类器预测正样本的精确率(在所有预测为正的样本中,正确预测的比例有多少),在其他指标不变的情况下,这个值越高,说明分类器的性能越好。
Recall与ROC曲线中的真正率TPR一样,也是正确预测为正的样本数量占所有实际为正的样本数量的比例,即TP / (TP + FN),它衡量的是分类器算法对正样本的预测能力(能将多少比例的正样本正确的分类出来),这个值越高分类器性能越好。
根据上面的公式,我们可以实现一个简易的计算PR曲线数据点的函数,具体的实现与ROC数据点计算的函数类似。
def simple_pr_curve(y_test, y_hat):
thresholds = sorted(set(y_hat))
precisions = []
recalls = []
for threshold in thresholds:
# 将大于阈值的预测概率值标为正样本(将其值设为1),否则标为负样本(将其值设为0)
y_pred = [1 if value >= threshold else 0 for value in y_hat]
tp, fp, tn, fn = cal_tpfn(y_test, y_pred)
# 计算精确率和召回率(真正率),召回率(真正率)为正确预测为正样本的数量与所有正样本的数量(测试集中的正样本)的比值
# 精确率为正确预测为正样本数量与所有正样本数量(测试集中的负样本)的比值
precision = tp / (tp + fp) if (tp + fp) != 0 else 0
recall = tp / (tp + fn) if (tp + fn) != 0 else 0
precisions.append(precision)
recalls.append(recall)
return precisions, recalls, thresholds
接着我们在计算PR曲线的AUC值。
直接使用sklearn库metrics模块中的average_precision_score()函数进行计算,输入参数为测试集样本实际标签值与测试集样本预测值)
pr_auc = average_precision_score(y_test, y_hat)
打印输出:
2. 绘制PR曲线
绘图代码与ROC曲线类似
plt.figure(figsize=(12, 5))
plt.plot(recalls, precisions, color='orange', lw=2, label='PR CURVE (auc = %0.2f)' % pr_auc)
plt.xlim([0, 1])
plt.ylim([0, 1.05])
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.title('Precision Recall Curve')
plt.legend(loc='lower right')
可以看出这里的图形出现了和ROC曲线类似的问题,这里的曲线只占整个范围的一半,没有将x轴和y轴完全包围,不利于我们观察,通常来说我们会在PR数据点中添加一个(0,1)值。
def simple_pr_curve(y_test, y_hat):
thresholds = sorted(set(y_hat))
precisions = []
recalls = []
for threshold in thresholds:
# 将大于阈值的预测概率值标为正样本(将其值设为1),否则标为负样本(将其值设为0)
y_pred = [1 if value >= threshold else 0 for value in y_hat]
tp, fp, tn, fn = cal_tpfn(y_test, y_pred)
# 计算精确率和召回率(真正率),召回率(真正率)为正确预测为正样本的数量与所有正样本的数量(测试集中的正样本)的比值
# 精确率为正确预测为正样本数量与所有正样本数量(测试集中的负样本)的比值
precision = tp / (tp + fp) if (tp + fp) != 0 else 0
recall = tp / (tp + fn) if (tp + fn) != 0 else 0
precisions.append(precision)
recalls.append(recall)
precisions.append(1.)
recalls.append(0.)
return precisions, recalls, thresholds
或者直接使用metrics模块下的precision_recall_curve()函数进行计算,该函数会在末尾加上(0,1)的数据点。
precisions, recalls, pr_thresholds = precision_recall_curve(y_test, y_hat)
重新绘制图形:
对Precision和Recall的值我们都希望越高越好,在曲线上的表现则是曲线越靠近右上角越好,因为这样分类器能在保持较高的准确率的同时拥有较高的召回率,在数值上的表现同样是曲线下面积auc,这里的auc值为0.89,是个很不错的值。
四、对不同分类器进行性能评估
1. 对不同k值的knn算法进行性能评估
要对不同k值的knn分类器进行性能评估,就要把它们的数据点全部算出来,可以将不同k值knn分类器保存在字典中,接着对字典中的所有分类器进行遍历,并计算各个分类器的数据点,最后再将各个分类器的k值以及数据点和auc保存进行另一个曲线数据字典中,画图时再从字典中依次取出各个分类器的数据进行绘图即可。
knn_classifier = {
3 : sklearn.neighbors.KNeighborsClassifier(n_neighbors=3),
5 : sklearn.neighbors.KNeighborsClassifier(n_neighbors=5),
10 : sklearn.neighbors.KNeighborsClassifier(n_neighbors=10),
15 : sklearn.neighbors.KNeighborsClassifier(n_neighbors=15)}
X, y = sklearn.datasets.make_classification(n_samples=1000, n_features=10, random_state=2)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=12)
roc_datas = {}
pr_datas = {}
for k, clf in knn_classifier.items():
# 训练
clf.fit(X_train, y_train)
# 对测试集进行预测
y_hat = clf.predict_proba(X_test)[:, 1]
# 计算ROC曲线数据点
fprs, tprs, roc_thresholds = simple_roc_curve(y_test, y_hat)
roc_auc = auc(fprs, tprs)
# 计算PR曲线数据点
precisions, recalls, pr_thresholds = simple_pr_curve(y_test, y_hat)
pr_auc = average_precision_score(y_test, y_hat)
# 将ROC和PR曲线的数据点添加进集合中
roc_datas[k] = (fprs, tprs, roc_auc)
pr_datas[k] = (recalls, precisions, pr_auc)
plt.figure(figsize=(12, 5))
for k, (fprs, tprs, roc_auc) in roc_datas.items():
plt.plot(fprs, tprs, lw=2, label='k = %d (auc = %0.3f)' % (k, roc_auc))
plt.plot([0, 1], [0, 1], color='blue', lw=2, linestyle='--')
plt.xlim([0, 1])
plt.ylim([0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve for Different k Values in k-NN Classifier')
plt.legend(loc='lower right')
绘制图形:
ROC曲线:
PR曲线:
对上面两个指标,我们可以看出k=15的时候auc值最高,不过其与k=10的auc值相差不多,因此k选择10左右就可以了。当然分类器的性能评估不止看这两个指标,这里只是根据这两个指标进行选择,实际性能可能并不是最好。
2. 对不同分类器进行性能评估
选取多个不同的分类器进行性能评估,对它们的性能进行比较,选取性能最好的分类器。
这里选择的分类器有:
逻辑回归(Logistic Regression)
K近邻(K-Nearest Neighbors)
梯度提升树(Gradient Boosting)
神经网络(Neural Network)
随机森林(Random Forest)
支持向量机(Support Vector Machine,SVM)
classifier2 = {
'Logistic Regression' : LogisticRegression(),
'KNN(k=10)' : sklearn.neighbors.KNeighborsClassifier(n_neighbors=10),
'Gradient Boosting' : GradientBoostingClassifier(),
'Neural Network' : MLPClassifier(),
'Random Forest' : RandomForestClassifier(),
'Support Vector Machine' : make_pipeline(StandardScaler(), SVC(probability=True)) }
ROC曲线
PR曲线
可以看出,对于两个指标,都是随机森林的效果最好,其次是逻辑回归。
因此若只根据这两个指标来选择分类器的话,选择随机森林或者逻辑回归最好。