机器学习系统模型调优实战
引言
如果你对机器学习算法已经很熟悉了,但是有时候你的模型并没有很好的预测效果或者你想要追求更好地模型性能。那么这篇文章会告诉你一些最实用的技术诊断你的模型出了什么样的问题,并用什么的方法来解决出现的问题,并通过一些有效的方法可以让你的模型具有更好地性能。
介绍数据集
这个数据集有569个样本,它的前两列为唯一的ID号和诊断结果 (M = malignant, B = benign) ,它的3->32列为实数值特征,我不是医学专家,我不太明白具体特征的是什么意思,都是关于细胞的,但是,机器学习的伟大之处就在于这点,即使我们不是一个这方面的专家,我们依然可以读懂这些数据,掌握数据中的模式,从而我们也可以像一个专家一样做出预测。
下面的链接有数据集更详细的介绍,有兴趣的朋友可以看看。
http://archive.ics.uci.edu/ml/datasets/Breast+Cancer+Wisconsin+(Diagnostic)
初识pipeline
在训练机器学习算法的过程中,我们用到了不同的数据预处理技术,比如:标准化,PCA等。在scikit-learn中,我们可以用pipeline这个非常方便的类帮我们简化这些过程。这个类帮我们用任意次的转换步骤来拟合模型并预测新的数据。下面,我用具体代码来演示这个类的好处:
import pandas as pd df = pd.read_csv('http://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/wdbc.data', header=None) # 读取数据集 from sklearn.preprocessing import LabelEncoder X = df.loc[:, 2:].values # 抽取训练集特征 y = df.loc[:, 1].values # 抽取训练集标签 le = LabelEncoder() y = le.fit_transform(y) # 把字符串标签转换为整数,恶性-1,良性-0 from sklearn.cross_validation import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, random_state=1) # 拆分成训练集(80%)和测试集(20%) # 下面,我要用逻辑回归拟合模型,并用标准化和PCA(30维->2维)对数据预处理,用Pipeline类把这些过程链接在一起 from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline # 用StandardScaler和PCA作为转换器,LogisticRegression作为评估器 estimators = [('scl', StandardScaler()), ('pca', PCA(n_components=2)), ('clf', LogisticRegression(random_state=1))] # Pipeline类接收一个包含元组的列表作为参数,每个元组的第一个值为任意的字符串标识符,比如:我们可以通过pipe_lr.named_steps['pca']来访问PCA组件;第二个值为scikit-learn的转换器或评估器 pipe_lr = Pipeline(estimators) pipe_lr.fit(X_train, y_train) print(pipe_lr.score(X_test, y_test))
当我们调用Pipeline的fit方法时,StandardScaler调用fit和transform方法来将数据标准化,接着标准化后的数据被传到PCA中,PCA也执行fit和transform方法对标准化后的数据进行降维,最后把降维的数据用逻辑回归模型拟合,当然了,这只是我的一个演示,你可以把任意多的转换步骤放到Pipeline中。而且,它的另一个优点是,当我们评估测试集的时候,它也会用上面转换过程保留的参数来转换测试集。这一切是不是很Cool?Pipeline就像一个工厂流水线一样,把所有的步骤链接到了一起,具体的细节完全不用我们自己操心。整个流程可以用下面的图形概括。
用holdout和k-fold交叉验证评估模型性能
holdout交叉验证
在我们上面的那个例子中,我们只把数据集分成了训练集和测试集。然而在实际的机器学习应用中,我们会选择最优的模型和最优的学习参数来提高我们算法对未见到的数据预测的性能。然而,如果我们在模型选择或调节参数的过程中,一遍又一遍地用我们的测试集,那么它也变成了我们测试集的一部分了,很有可能这样的参数和模型只适应当前的测试集,而对没有见过的数据集它的性能很不好,也就是发生了过拟合现象。
因此,一个更好地方式是把我们的数据集分成三个部分:把上面例子中的训练集分成训练集和交叉验证集,测试集。现在,我们构建机器学习的系统步骤应该是:
- 选择模型和参数,用训练集去拟合
- 把拟合后的模型和参数应用到交叉验证集来评估其性能
- 不断地重复1,2两个过程,直到挑选出我们满意的模型和参数
- 用测试集去评估步骤3选出的模型,看它在未见过的数据上的性能
但是,holdout交叉验证有个缺点是:样本不同的训练集和交叉验证集会产生不同的性能评估。下面,让我们介绍k-fold交叉验证来解决这个问题。
k-fold交叉验证
在holdout交叉验证中,我们把训练集拆分成训练集和交叉验证集。在k-fold交叉验证中,我们把训练集拆分成k份,k-1份用作训练,1份用作测试。然后,我们把这k份中的每份都用来测试剩下的份用作训练,因此,我们会得到k个模型和性能的评估,最后求出性能的平均值。下面,我假设k=10,看下图:
上图中,我把训练集分成10份,在10次迭代中,9份被用作训练,1份被用作测试,最后我们求出平均性能。注意:在每次迭代中,我们并没有重新划分训练集,我们只是最初分成10份,接着在每次迭代中,用这10份中的每一份做测试剩下的9份用作训练。
对于大多数的应用k=10是个合理的选择。然而,如果我们的训练集相对较小,我们可以增加k值,因此,在每次迭代中我们将有更多的训练集来拟合模型,结果是我们对泛化的性能有更低地偏差。另一方面,如果我们有更大地数据集,我们可以选择一个更小地值k,即使k值变小了,我们依然可以得到一个对模型性能的准确评估,与此同时,我们还减少了重新拟合模型的计算代价。
在scikit-learn的实现中,它对我们的k-fold交叉验证做了一个小小的改进,它在每个份的训练集中都有相同的类别比例,请看如下代码:
from sklearn.cross_validation import StratifiedKFold import numpy as np scores = [] kfold = StratifiedKFold(y=y_train, n_folds=10, random_state=1) # n_folds参数设置为10份 for train_index, test_index in kfold: pipe_lr.fit(X_train[train_index], y_train[train_index]) score = pipe_lr.score(X_train[test_index], y_train[test_index]) scores.append(score) print('类别分布: %s, 准确度: %.3f' % (np.bincount(y_train[train_index]), score)) np.mean(scores) # 求出评估的平均值,0.94956521739130439 # 输出如下: 类别分布: [257 153], 准确度: 0.891 类别分布: [257 153], 准确度: 0.978 类别分布: [257 153], 准确度: 0.978 类别分布: [257 153], 准确度: 0.913 类别分布: [257 153], 准确度: 0.935 类别分布: [257 153], 准确度: 0.978 类别分布: [257 153], 准确度: 0.933 类别分布: [257 153], 准确度: 0.956 类别分布: [257 153], 准确度: 0.978 类别分布: [257 153], 准确度: 0.956
上面,我们自己写for循环去拟合每个训练集。scikit-learn有一种更有效地方式帮我们实现了上述的方法:
from sklearn.cross_validation import cross_val_score scores = cross_val_score(estimator=pipe_lr, X=X_train, y=y_train, cv=10, n_jobs=1) print(scores) # 输出如下: [ 0.89130435 0.97826087 0.97826087 0.91304348 0.93478261 0.97777778 0.93333333 0.95555556 0.97777778 0.95555556]
上面的n_jobs参数可以指定我们机器上的多个CPU来评估我们每份不同的训练集,这是一个非常有用的参数。
学习曲线和验证曲线
学习曲线
通过绘制模型训练和验证准确性关于训练集大小的函数,我们能很容易地诊断出模型是高方差还是高偏差。
左上角的那个图像是高偏差,这个模型的训练集准确性和交叉验证集准确性都很低,这表明它欠拟合数据。解决高偏差问题通常要增加模型的参数,比如,构建更多的样本特征或减小正则化的程度。
右上角的那个图像是高方差,这个模型的训练集准确性和交叉验证集准确性之间有个很大的缺口,这表明模型很好地拟合了训练集,但是对未见过的数据效果很差。对于过拟合问题,我们可以收集更多地数据或降低模型的复杂度。有一点我们应该注意,如果训练集有很多的噪音或模型已经接近最优性能了,我们收集在多的数据也于事无补。
下面,我们用具体的代码来绘制学习曲线。
from sklearn.learning_curve import learning_curve pipe_lr = Pipeline([('scl', StandardScaler()), ('clf', LogisticRegression(penalty='l2', random_state=0))]) # train_sizes参数指定用于生成学习曲线的训练集数量,如果是分数指的是相对数量,整数指的是绝对数量 train_sizes, train_scores, test_scores = learning_curve(estimator=pipe_lr, X=X_train, y=y_train, train_sizes=np.linspace(0.1, 1.0, 10), cv=10, n_jobs=1) train_mean = np.mean(train_scores, axis=1) train_std = np.std(train_scores, axis=1) test_mean = np.mean(test_scores, axis=1) test_std = np.std(test_scores, axis=1) plt.plot(train_sizes, train_mean, color='blue', marker='o', markersize=5, label='training accuracy') plt.fill_between(train_sizes, train_mean + train_std, train_mean - train_std, alpha=0.15, color='blue') plt.plot(train_sizes, test_mean, color='green', linestyle='--', marker='s', markersize=5, label='validation accuracy') plt.fill_between(train_sizes, test_mean + test_std, test_mean - test_std, alpha=0.15, color='green') plt.grid() plt.xlabel('Number of training samples') plt.ylabel('Accuracy') plt.legend(loc='lower right') plt.ylim([0.8, 1.0]) plt.show()
从上图我们可以看出,模型在交叉训练集上表现地很好。但是,它是有点过拟合的,因为在两个曲线之间有一点明显地间隔。
验证曲线
学习曲线是训练集数量与准确性之间的函数。而验证曲线是不同的模型参数与准确性之间的函数。具体代码如下:
from sklearn.learning_curve import validation_curve param_range = [0.001, 0.01, 0.1, 1.0, 10.0, 100.0] train_scores, test_scores = validation_curve(estimator=pipe_lr, X=X_train, y=y_train, param_name='clf__C', param_range=param_range, cv=10) train_mean = np.mean(train_scores, axis=1) train_std = np.std(train_scores, axis=1) test_mean = np.mean(test_scores, axis=1) test_std = np.std(test_scores, axis=1) plt.plot(param_range, train_mean, color='blue', marker='o', markersize=5, label='training accuracy') plt.fill_between(param_range, train_mean + train_std, train_mean - train_std, alpha=0.15, color='blue') plt.plot(param_range, test_mean, color='green', linestyle='--', marker='s', markersize=5, label='validation accuracy') plt.fill_between(param_range, test_mean + test_std, test_mean - test_std, alpha=0.15, color='green') plt.grid() plt.xscale('log') plt.legend(loc='lower right') plt.xlabel('Parameter C') plt.ylabel('Accuracy') plt.ylim([0.8, 1.0]) plt.show()
上图我们可以看到随着参数C的增大,模型有点过拟合数据,因为C越大,就意味着正则化的强度越小。然而,对于小的参数C来说,正则化的强度很大,模型有点欠拟合。我感觉当C在0.1左右是最好的。
通过网格搜索(grid search)微调机器学习模型
在机器学习应用中,我们有两种类型的参数:一个是从训练集中学得的参数,例如逻辑回归的权重;另一个是为了使学习算法达到最优化可调节的参数,例如逻辑回归中的正则化参数或决策树中的深度参数。这种可调节的参数称为超参数(hyperparameters)。
上面我们用验证曲线调节超参数中的一个参数来优化模型。现在,我们要用网格搜索这个更加强大的超参数优化工具来找到超参数值的最优组合从而进一步改善模型的性能。
网格搜索的思路其实很简单,就是列举出所有你想要调节的参数,然后穷举出所有参数组合,最后得出一个使模型性能最好的参数组合。下面,让我们来调节SVM分类器的C,kernel,gamma参数,代码如下:
from sklearn.grid_search import GridSearchCV from sklearn.svm import SVC pipe_svc = Pipeline([('scl', StandardScaler()), ('clf', SVC(random_state=1))]) param_range = [0.0001, 0.001, 0.01, 0.1, 1.0, 10.0, 100.0, 1000.0] param_grid = [{'clf__C': param_range, 'clf__kernel': ['linear']}, {'clf__C': param_range, 'clf__gamma': param_range, 'clf__kernel': ['rbf']}] gs = GridSearchCV(estimator=pipe_svc, param_grid=param_grid, scoring='accuracy', cv=10, n_jobs=-1) gs = gs.fit(X_train, y_train) print(gs.best_score_) print(gs.best_params_) clf = gs.best_estimator_ clf.fit(X_train, y_train) print('Test accuracy: %.3f' % clf.score(X_test, y_test))
性能指标之 precision, recall, 和F1-score
度量模型的性能指标不仅仅是它的准确性,还有 precision, recall, 和F1-score。下图的四个方块中分别计算了对应情况的数量,我们可以把它看作是一个2 × 2的矩阵。
scikit-learn可以很容易地得出上面的矩阵形式。代码如下:
from sklearn.metrics import confusion_matrix pipe_svc.fit(X_train, y_train) y_pred = pipe_svc.predict(X_test) confmat = confusion_matrix(y_true=y_test, y_pred=y_pred) print(confmat) # 输出结果如下: [[71 1] [ 2 40]]
用matplotlib的matshow函数更加形象地演示上面的矩阵。
fig, ax = plt.subplots() ax.matshow(confmat, cmap=plt.cm.Blues, alpha=0.3) for i in range(confmat.shape[0]): for j in range(confmat.shape[1]): ax.text(x=j, y=i, s=confmat[i, j], va='center', ha='center') plt.xlabel('predicted label') plt.ylabel('true label') plt.show()
假设类别1(恶性)为positive类别,那么我们的模型正确地分类属于类别0的71个样本(true negatives) 和属于类别1的40个样本(true positives)。然而,我们的模型也错误地分类原本属于类别1而预测为0的2个样本(false negatives)和原本属于类别0而预测为1的1个样本(false positive)。接下来,我们用这个信息来计算不同的误差评价标准。
在类别很不平衡的机器学习系统中,我们通常用precision(PRE)和recall(REC)来度量模型的性能,下面我给出它们的公式:
在实际中,我们通常结合两者,组成F1-score:
上面3种测量手段,在scikit-learn中都已经实现:
from sklearn.metrics import precision_score, recall_score, f1_score precision_score(y_true=y_test, y_pred=y_pred) recall_score(y_true=y_test, y_pred=y_pred) f1_score(y_true=y_test, y_pred=y_pred)
ROC
在介绍ROC曲线前,我先给出true positive rate(TPR)和false positive rate(FPR)的定义:
ROC是一种选择分类模型的工具,它是基于true positive rate(TPR)和false positive rate(FPR)的性能来做出选择的。我们通过移动分类器的决策阙值来计算TPR和FPR。ROC图像上的对角线可以看作是随机猜测的结果,如果分类模型在对角线的下面则证明它的性能比随机猜测的结果还要糟糕。基于ROC曲线,我们可以计算出描述分类模型性能的AUC(area under the curve)。在ROC曲线中,左下角的点所对应的是将所有样例判为反例的情况,而右上角的点对应的则是将所有样例判为正例的情况。
下面的代码绘制了ROC曲线。
from sklearn.metrics import roc_curve, auc from scipy import interp X_train2 = X_train[:, [4, 14]] cv = StratifiedKFold(y_train, n_folds=3, random_state=1) fig = plt.figure() mean_tpr = 0.0 mean_fpr = np.linspace(0, 1, 100) all_tpr = [] # plot每个fold的ROC曲线,这里fold的数量为3,被StratifiedKFold指定 for i, (train, test) in enumerate(cv): # 返回预测的每个类别(这里为0或1)的概率 probas = pipe_lr.fit(X_train2[train], y_train[train]).predict_proba(X_train2[test]) fpr, tpr, thresholds = roc_curve(y_train[test], probas[:, 1], pos_label=1) mean_tpr += interp(mean_fpr, fpr, tpr) mean_tpr[0] = 0.0 roc_auc = auc(fpr, tpr) plt.plot(fpr, tpr, linewidth=1, label='ROC fold %d (area = %0.2f)' % (i+1, roc_auc)) # plot random guessing line plt.plot([0, 1], [0, 1], linestyle='--', color=(0.6, 0.6, 0.6), label='random guessing') mean_tpr /= len(cv) mean_tpr[-1] = 1.0 mean_auc = auc(mean_fpr, mean_tpr) plt.plot(mean_fpr, mean_tpr, 'k--', label='mean ROC (area = %0.2f)' % mean_auc, lw=2) # plot perfect performance line plt.plot([0, 0, 1], [0, 1, 1], lw=2, linestyle=':', color='black', label='perfect performance') # 设置x,y坐标范围 plt.xlim([-0.05, 1.05]) plt.ylim([-0.05, 1.05]) plt.xlabel('false positive rate') plt.ylabel('true positive rate') plt.title('Receiver Operator Characteristic') plt.legend(loc="lower right") plt.show()
上面代码中涉及到roc_curve类,详情请参考官方文档:http://scikit-learn.org/stable/modules/generated/sklearn.metrics.roc_curve.html