MNIST数据集:二分类问题
MNIST数据集是一组由美国高中生和人口调查局员工手写的70,000个数字的图片,每张图片上面有代表的数字标记。
这个数据集被广泛使用,被称之为机器学习领域的“Hello World”,主要是被用于分类问题。本文是对MNIST数据集执行一个二分类的建模
关键词:随机梯度下降、二元分类、混淆矩阵、召回率、精度、性能评估
导入数据
在这里是将一份存放在本地的mat文件的数据导进来:
In [1]:
1 | import pandas as pd |
In [2]:
1 | mnist = si.loadmat('mnist-original.mat') |
In [3]:
1 | type(mnist) # 查看数据类型 |
Out[3]:
1 | dict |
In [4]:
1 | mnist.keys() |
Out[4]:
1 | dict_keys(['__header__', '__version__', '__globals__', 'mldata_descr_ordering', 'data', 'label']) |
我们发现导进来的数据是一个字典。其中data和label两个键的值就是我们想要的特征和标签数据
创建特征和标签
In [5]:
1 | # 修改1:一定要转置 |
Out[5]:
1 | (70000, 784) |
总共是70000张图片,每个图片中有784个特征。图片是28*28的像素,所以每个特征代表一个像素点,取值从0-255。
In [6]:
1 | y.shape |
Out[6]:
1 | (70000, 1) |
In [7]:
1 | y # 每个图片有个专属的数字 |
Out[7]:
1 | array([[0.], |
显示一张图片
In [8]:
1 | import matplotlib as mpl |
In [9]:
1 | y[0] # 真实的标签的确是0 |
Out[9]:
1 | array([0.]) # 结果是0 |
标签类型转换
元数据中标签是字符串,我们需要转成整数类型
In [10]:
1 | y.dtype |
Out[10]:
1 | dtype('<f8') |
In [11]:
1 | y = y.astype(np.uint8) |
创建训练集和测试集
前面的6万条是训练集,后面的1万条是测试集
In [12]:
1 | X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:] |
二元分类器
比如现在有1张图片,显示是0,我们识别是:“0和非0”,两种情形即可,这就是简单的二元分类问题
In [13]:
1 | y_train_0 = (y_train == 0) # 挑选出5的部分 |
随机梯度下降分类器SGD
使用scikit-learn自带的SGDClassifier分类器:能够处理非常大型的数据集,同时SGD适合在线学习
In [14]:
1 | from sklearn.linear_model import SGDClassifier |
Out[14]:
1 | SGDClassifier(random_state=42) |
结果验证
在这里我们检查下数字0的图片:结果为True
In [15]:
1 | sgd_c.predict([one_digit]) # one_digit是0,非5 表示为False |
Out[15]:
1 | array([ True]) |
性能测量1-交叉验证
一般而言,分类问题的评估比回归问题要困难的多。
自定义交差验证(优化)
- 每个折叠由StratifiedKFold执行分层抽样,产生的每个类别中的比例符合原始数据中的比例
- 每次迭代会创建一个分类器的副本,用训练器对这个副本进行训练,然后测试集进行测试
- 最后预测出准确率,输出正确的比例
In [16]:
1 | # K折交叉验证 |
运行的结果如下:
1 | [0.09875 0.09875 0.09875 ... 0.90125 0.90125 0.90125] |
scikit_learn的交叉验证
使用cross_val_score来评估分类器:
In [17]:
1 | # 评估分类器的效果 |
可以看到准确率已经达到了95%以上,效果是相当的可观
自定义一个“非0”的简易分类器,看看效果:
In [18]:
1 | from sklearn.base import BaseEstimator # 基分类器 |
In [19]:
1 | never_0_clf = Never0Classifier() |
Out[19]:
1 | array([0.70385, 1. , 1. ]) |
In [20]:
统计数据中每个字出现的次数:
1 | pd.DataFrame(y).value_counts() |
Out[20]:
1 | 1 7877 |
In [21]:
1 | 6903 / 70000 |
Out[21]:
下面显示大约有10%的概率是0这个数字
1 | 0.09861428571428571 |
In [22]:
1 | (0.70385 + 1 + 1) / 3 |
Out[22]:
1 | 0.9012833333333333 |
可以看到判断“非0”准确率基本在90%左右,因为只有大约10%的样本是属于数字0。
所以如果猜测一张图片是非0,大约90%的概率是正确的。
性能测量2-混淆矩阵
预测结果
评估分类器性能更好的方法是混淆矩阵,总体思路是统计A类别实例被划分成B类别的次数
混淆矩阵是通过预测值和真实目标值来进行比较的。
cross_val_predict函数返回的是每个折叠的预测结果,而不是评估分数
In [23]:
1 | from sklearn.model_selection import cross_val_predict |
Out[23]:
1 | array([ True, True, True, ..., False, False, False]) |
混淆矩阵
In [24]:
1 | # 导入混淆矩阵 |
Out[24]:
1 | array([[52482, 1595], |
混淆矩阵中:行表示实际类别,列表示预测类别
- 第一行表示“非0”:52482张被正确地分为“非0”(真负类),有1595张被错误的分成了“0”(假负类)
- 第二行表示“0”:267被错误地分为“非0”(假正类),有5656张被正确地分成了“0”(真正类)
In [25]:
1 | # 假设一个完美的分类器:只存在真正类和真负类,它的值存在于对角线上 |
Out[25]:
1 | array([[54077, 0], |
精度和召回率
$$精度=\frac{TP}{TP+FP}$$
召回率的公式为:
$$召回率 = \frac {TP}{TP+FN}$$
混淆矩阵显示的内容:
- 左上:真负
- 右上:假正
- 左下:假负
- 右下:真正
精度:正类预测的准确率
召回率(灵敏度或真正类率):分类器正确检测到正类实例的比例
计算精度和召回率
In [26]:
1 | from sklearn.metrics import precision_score, recall_score |
Out[26]:
1 | 0.78003034064267 |
In [27]:
1 | recall_score(y_train_0, y_train_pred) # 召回率 |
Out[27]:
1 | 0.9549214924869154 |
F_1系数
F_1系数是精度和召回率的谐波平均值。只有当召回率和精度都很高的时候,分类器才会得到较高的F_1分数
𝐹1=21精度+1召回率(3)(3)F1=21精度+1召回率
In [28]:
1 | from sklearn.metrics import f1_score |
Out[28]:
1 | 0.8586609989373006 |
精度/召回率权衡
精度和召回率通常是一对”抗体“,我们一般不可能同时增加精度又减少召回率,反之亦然,这就现象叫做精度/召回率权衡
In [29]:
1 | # 使用decision_function |
Out[29]:
1 | array([24816.66593936]) |
In [30]:
1 | threshold = 0 # 设置阈值 |
Out[30]:
1 | array([ True]) |
In [31]:
1 | # 提升阈值 |
Out[31]:
1 | array([False]) |
如何使用阈值
- 先使用cross_val_predict函数获取训练集中所有实例的分数
In [32]:
1 | y_scores = cross_val_predict( |
Out[32]:
1 | array([ 51616.39393745, 27082.28092103, 20211.29278048, ..., |
有了这些分数就可以计算精度和召回率:
In [33]:
1 | from sklearn.metrics import precision_recall_curve |
In [34]:
1 | precisions # 精度 |
Out[34]:
1 | array([0.10266944, 0.10265389, 0.10265566, ..., 1. , 1. , |
In [35]:
1 | recalls # 召回率 |
Out[35]:
1 | array([1.00000000e+00, 9.99831167e-01, 9.99831167e-01, ..., |
In [36]:
1 | thresholds # 阈值 |
Out[36]:
1 | array([-86393.49001095, -86375.60229796, -86374.22313529, ..., |
绘制精度和召回率曲线
In [37]:
1 | def figure_precision_recall(precisions, recalls, thresholds): |
直接绘制精度和召回率的曲线图:
1 | # 精度-召回率 |
现在我们将精度设置成90%,通过np.argmax()函数来获取最大值的第一个索引,即表示第一个True的值:
In [39]:
1 | threshold_90_precision = thresholds[np.argmax(precisions >= 0.9)] |
Out[39]:
1 | 9075.648564157285 |
In [40]:
1 | y_train_pred_90 = (y_scores >= threshold_90_precision) |
Out[40]:
1 | array([ True, True, True, ..., False, False, False]) |
In [41]:
1 | # 再次查看精度和召回率 |
Out[41]:
1 | 0.9001007387508395 |
In [42]:
1 | recall_score(y_train_0, y_train_pred_90) |
Out[42]:
1 | 0.9051156508526085 |
性能测量3-ROC曲线
绘制ROC
还有一种经常和二元分类器一起使用的工具,叫做受试者工作特征曲线ROC。
绘制的是真正类率(召回率的别称)和假正类率(FPR)。FPR是被错误分为正类的负类实例比率,等于1减去真负类率(TNR)
TNR是被正确地分为负类的负类实例比率,也称之为特异度。
ROC绘制的是灵敏度和(1-特异度)的关系图
In [43]:
1 | # 1、计算TPR、FPR |
In [44]:
1 | def plot_roc_curve(fpr,tpr,label=None): |
AUC面积
auc就是上面ROC曲线的线下面积。完美的分类器ROC_AUC等于1;纯随机分类器的ROC_AUC等于0.5
In [45]:
1 | from sklearn.metrics import roc_auc_score |
Out[45]:
1 | 0.9910680354987216 |
ROC曲线和精度/召回率(PR)曲线非常类似,选择经验:当正类非常少见或者我们更加关注假正类而不是假负类,应该选择PR曲线,否则选择ROC曲线
对比随机森林分类器
报错:index 1 is out of bounds for axis 1 with size 1
In [46]:
1 | X_train.shape |
Out[46]:
1 | (60000, 784) |
In [47]:
1 | # 解决方案 |
Out[47]:
1 | array([ True, True, True, ..., False, False, False]) |
In [48]:
1 | from sklearn.ensemble import RandomForestClassifier |
Out[48]:
1 | array([[0. , 1. ], |
使用roc_curve函数来提供分类的概率:
In [49]:
1 | y_scores_forest = y_probas_forest[:,1] |
In [50]:
1 | plt.plot(fpr, tpr, "b:", label="SGD") |
现在我们重新查看ROC-AUC值、精度和召回率,发现都得到了提升:
In [51]:
1 | roc_auc_score(y_train_0,y_scores_forest) # ROC-AUC值 |
Out[51]:
1 | 0.9975104189747056 |
In [52]:
1 | precision_score(y_train_0,y_train_pred) # 精度 |
Out[52]:
1 | 0.78003034064267 |
In [53]:
1 | recall_score(y_train_0,y_train_pred) # 召回率 |
Out[53]:
1 | 0.9549214924869154 |
总结
本文从公开的MNIST数据出发,通过SGD建立一个二元分类器,同时利用交叉验证来评估我们的分类器,以及使用不同的指标(精度、召回率、精度/召回率平衡)、ROC曲线等来比较SGD和RandomForestClassifier不同的模型。