MNIST数据集介绍
MNIST是一个手写数字数据集,数据集包含60000个训练数据与10000个测试数据,由500位作者手写而成,其中250位的手写作为训练数据,其余250位的手写作为测试数据。
通过这里MNIST数据集获取数据。
根据官方描述,该数据集的每个数据都被统一格式化成28 * 28像素大小,并已将手写数字居中于图像中央,即我们无需对数据集进行过多的预处理。
观察数据集可以发现,训练集60000,测试集10000,每张图片拥有784个特征。
X_train, y_train = load_train()
X_test, y_test = load_test()
print(X_train.shape) # (60000, 784) (10000, 784)
print(y_train.shape) # (60000,) (10000,)
使用matplotlib绘制其中一张图如下
image = X_train[0].reshape(28, 28)
plt.imshow(image, cmap='binary', interpolation="nearest")
plt.show()
显而易见,训练集的第一张手写图是数字“5”
训练思路
降维
通过观察数据发现,每张图片的边缘存在大量空白,即使删掉这部分内容,也不会丢失太多信息。针对这种情况,考虑使用降维来去除这些无关信息,降维还能够将两个相邻特征合并为一个,以此来降低维度,提升训练速度。
选择模型
本文将尝试使用几种模型对数据集进行训练:K邻近、SVM(支持向量机)、随机森林
其中,由于SVM是一个二元分类器,所以将会对10个数字分别训练10个分类器。
为了给模型找到合适的超参数,以更好的拟合数据,则使用GridSearchCV(网格搜索)对超参数进行微调。
最后采用模型集成来做预测。
评估模型
通过两种方式来评估模型性能,分别是交叉验证与混淆矩阵。
训练模型
PCA
使用PCA(主成分分析)降维,问题在于如何设置合适的维度,n_components参数用于设置需要保留的主成分数;
若不确定其主成分数,可以设置值为[0.0, 1.0],此时值表示要保留的方差率。
from sklearn.decomposition import PCA
# 使用主成分分析降维,保留95%的可解释方差比
pca = PCA(n_components=0.95)
X_train = pca.fit_transform(X_train)
print(pca.n_components_) # 输出154,即保留了154个主成分
KNN
KNN(K-邻近值)感觉是比较适合这项分类任务的模型。
使用sklearn提供的网格搜索,找出合适的模型超参数。
网格搜索
from sklearn.model_selection import GridSearchCV
# 尝试以下参数组合
param_grid= [
{'weights': ['uniform', 'distance'], 'n_neighbors': [5, 10, 15]}
]
grid_search = GridSearchCV(model, param_grid, cv=3, scoring='neg_mean_squared_error', return_train_score=True, n_jobs=-1)
grid_search.fit(X_train, y_train)
# 打印最佳超参数组合
print("best_params : ", grid_search.best_params_)
# 打印最佳超参数模型
print("best_estimator : ", grid_search.best_estimator_)
# 打印所有超参数组合分值
cvres = grid_search.cv_results_
for mean_score, params in zip(cvres["mean_test_score"], cvres["params"]):
print(np.sqrt(-mean_score), params)
# 输出结果如下
best_params : {'n_neighbors': 5, 'weights': 'distance'}
best_estimator : KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski',
metric_params=None, n_jobs=None, n_neighbors=5, p=2,
weights='distance')
1.07060730429042 {'n_neighbors': 5, 'weights': 'uniform'}
1.031649165171959 {'n_neighbors': 5, 'weights': 'distance'}
1.1418844074598795 {'n_neighbors': 10, 'weights': 'uniform'}
1.1020435563080073 {'n_neighbors': 10, 'weights': 'distance'}
1.20797350964332 {'n_neighbors': 15, 'weights': 'uniform'}
1.1664904628842878 {'n_neighbors': 15, 'weights': 'distance'}
查看搜索结果可以发现,最佳的超参数组合是{'n_neighbors': 5, 'weights': 'distance'}
,接下来可以设置n_neighbors小于5的值继续尝试,直到找到最佳模型的超参数。
最终找到合适的超参数时,训练KNN模型,并使用交叉验证观察结果
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import cross_val_score
kn_clf = KNeighborsClassifier(n_neighbors=4, weights='distance', n_jobs=-1)
# 进行3次交叉验证
scores = cross_val_score(kn_clf, X_train, y_train, cv=3, n_jobs=-1, scoring='accuracy')
# 输出每次验证的正确率
print(scores)
# 输出平均正确率
print(scores.mean())
# 输出结果如下
[0.97330534 0.97069853 0.97154573]
0.9718498685729129
97%,可以看出,KNN模型在交叉验证上获取了不错的正确率。
继续使用同样的思路训练以下模型
SVM
SVM是二元分类器,有一种策略是为每个数字训练一个二元分类器,比如数字“0”分类器,只判断是0与不是0;针对该数据集则需要训练10个分类器。
使用OneVsRestClassifier
类将模型包装成一对多的二元分类器。
from sklearn.svm import SVC
from sklearn.multiclass import OneVsRestClassifier
svm_clf = OneVsRestClassifier(SVC())
随机森林
from sklearn.ensemble import RandomForestClassifier
# 训练500颗决策树,最大深度为16
forest_clf = RandomForestClassifier(n_estimators=500, max_depth=16, random_state=42, n_jobs=-1)
# 交叉验证结果如下
[0.92420611 0.94269427 0.96455392]
0.9438181003223488
随机森林在测试数据集上获得了94.3%的正确率。
集成模型
将多个模型集成为一个模型,以此预测出的结果是综合了子模型的结果得出的,这种方法有助于提高正确性。
from sklearn.ensemble import VotingClassifier
kn_clf = KNeighborsClassifier(n_neighbors=4, weights='distance', n_jobs=-1)
svm_clf = SVC(probability=True)
forest_clf = RandomForestClassifier(n_estimators=500, max_depth=16, random_state=42, n_jobs=-1)
voting = VotingClassifier(estimators=[
('knn', kn_clf),
('svm', svm_clf),
('tree', forest_clf)
], voting='soft')
voting.fit(X_train, y_train)
评估模型
通常训练好的模型在投入生产使用之前,需要对其性能做评估,虽然我们的模型通过交叉验证得到了不错的结果,但模型很可能过拟合,还有一种评估分类器性能的好方法是混淆矩阵
混淆矩阵
该方法的思路是检查A类别被判断为B类别的次数,若次数过高,则需要采取措施降低混淆的次数。
为了建立混淆矩阵,首先需要一组预测数据,然后调用confusion_matrix()
建立混淆矩阵
from sklearn.model_selection import cross_val_predict
from sklearn.metrics import confusion_matrix
# 得到一组预测值
y_predict = cross_val_predict(kn_clf, X_train, y_train, cv=3)
# 建立混淆矩阵
matrix = confusion_matrix(y_train, y_predict)
混淆矩阵输出的是所有类别两两之间判断正确与错误的个数,用MNIST的测试集计算出的混淆矩阵做个示例:
[[ 962 1 0 0 0 5 10 1 1 0]
[ 0 1127 2 2 0 0 4 0 0 0]
[ 11 20 947 8 3 3 5 28 5 2]
[ 0 3 8 950 0 16 1 12 14 6]
[ 0 16 2 0 911 1 5 1 0 46]
[ 3 8 1 30 2 822 11 2 5 8]
[ 10 4 1 1 3 2 936 0 1 0]
[ 0 34 0 0 1 3 0 965 1 24]
[ 7 10 7 20 14 24 4 10 868 10]
[ 5 8 3 9 22 4 1 29 6 922]]
可以看到,这是一个10 * 10的矩阵,1-10列代表数字0-9,第一行表示数字0被正确分类的个数为962,被误分类成数字1的个数是1,被误分类成数字2的个数是0,以此类推…
用matplotlib将矩阵画出来,可以直观地看出数据情况,颜色越亮表示该数字被正确分类的个数越多,我们需要关注的是颜色相比暗淡的数字,比如数字5,说明数字5被正确分类的数量较少,即被错误分类的数量较多。
通过观察混淆矩阵能够有针对性的继续改进模型,比如对数据进行预处理,或者适当地增加样本数量等。
测试集
当模型在交叉验证与混淆矩阵表现都不错的时候,可以用上测试数据集了,测试集对于模型来说是陌生的,若模型过拟合训练数据,则很容易在测试集上表现差。
# 计算模型在测试集上的准确性
predict = model.predict(X_test)
correct = sum(predict == y_test)
print(correct / len(y_test))
结语
机器学习的过程,需要不停地探索,从数据预处理,到模型优化,甚至是花更多精力在收集整理数据集上,需要不断通过结果反馈来修正训练过程。有经验的大佬们,应该会花更少的时间就能训练出很好的模型,训练一个模型的成本还是比较高的,即又花费硬件资源,又花费时间。