前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深度学习实战-MNIST数据集的二分类

深度学习实战-MNIST数据集的二分类

作者头像
皮大大
发布2023-08-25 10:41:34
3970
发布2023-08-25 10:41:34
举报

MNIST数据集:二分类问题

MNIST数据集是一组由美国高中生和人口调查局员工手写的70,000个数字的图片,每张图片上面有代表的数字标记。

这个数据集被广泛使用,被称之为机器学习领域的“Hello World”,主要是被用于分类问题。本文是对MNIST数据集执行一个二分类的建模

关键词:随机梯度下降、二元分类、混淆矩阵、召回率、精度、性能评估

导入数据

在这里是将一份存放在本地的mat文件的数据导进来:

In [1]:

代码语言:javascript
复制
import pandas as pd
import numpy as np

import scipy.io as si
# from sklearn.datasets import fetch_openml

In [2]:

代码语言:javascript
复制
mnist = si.loadmat('mnist-original.mat')

In [3]:

代码语言:javascript
复制
type(mnist)   # 查看数据类型

Out[3]:

代码语言:javascript
复制
dict

In [4]:

代码语言:javascript
复制
mnist.keys()

Out[4]:

代码语言:javascript
复制
dict_keys(['__header__', '__version__', '__globals__', 'mldata_descr_ordering', 'data', 'label'])

我们发现导进来的数据是一个字典。其中data和label两个键的值就是我们想要的特征和标签数据

创建特征和标签

In [5]:

代码语言:javascript
复制
# 修改1:一定要转置

X, y = mnist["data"].T, mnist["label"].T

X.shape

Out[5]:

代码语言:javascript
复制
(70000, 784)

总共是70000张图片,每个图片中有784个特征。图片是28*28的像素,所以每个特征代表一个像素点,取值从0-255。

In [6]:

代码语言:javascript
复制
y.shape

Out[6]:

代码语言:javascript
复制
(70000, 1)

In [7]:

代码语言:javascript
复制
y   # 每个图片有个专属的数字

Out[7]:

代码语言:javascript
复制
array([[0.],
       [0.],
       [0.],
       ...,
       [9.],
       [9.],
       [9.]])

显示一张图片

In [8]:

代码语言:javascript
复制
import matplotlib as mpl
import matplotlib.pyplot as plt

one_digit = X[0]

one_digit_image = one_digit.reshape(28, 28)

plt.imshow(one_digit_image, cmap="binary")
plt.axis("off")
plt.show()

In [9]:

代码语言:javascript
复制
y[0]    # 真实的标签的确是0

Out[9]:

代码语言:javascript
复制
array([0.])  # 结果是0

标签类型转换

元数据中标签是字符串,我们需要转成整数类型

In [10]:

代码语言:javascript
复制
y.dtype

Out[10]:

代码语言:javascript
复制
dtype('<f8')

In [11]:

代码语言:javascript
复制
y = y.astype(np.uint8)

创建训练集和测试集

前面的6万条是训练集,后面的1万条是测试集

In [12]:

代码语言:javascript
复制
X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]

二元分类器

比如现在有1张图片,显示是0,我们识别是:“0和非0”,两种情形即可,这就是简单的二元分类问题

In [13]:

代码语言:javascript
复制
y_train_0 = (y_train == 0)  # 挑选出5的部分

y_test_0 = (y_test == 0)

随机梯度下降分类器SGD

使用scikit-learn自带的SGDClassifier分类器:能够处理非常大型的数据集,同时SGD适合在线学习

In [14]:

代码语言:javascript
复制
from sklearn.linear_model import SGDClassifier

sgd_c = SGDClassifier(random_state=42)  # 设置随机种子,保证运行结果相同

sgd_c.fit(X_train, y_train_0)
/Applications/downloads/anaconda/anaconda3/lib/python3.7/site-packages/sklearn/utils/validation.py:993: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().
  y = column_or_1d(y, warn=True)

Out[14]:

代码语言:javascript
复制
SGDClassifier(random_state=42)

结果验证

在这里我们检查下数字0的图片:结果为True

In [15]:

代码语言:javascript
复制
sgd_c.predict([one_digit])  # one_digit是0,非5 表示为False

Out[15]:

代码语言:javascript
复制
array([ True])

性能测量1-交叉验证

一般而言,分类问题的评估比回归问题要困难的多。

自定义交差验证(优化)

  • 每个折叠由StratifiedKFold执行分层抽样,产生的每个类别中的比例符合原始数据中的比例
  • 每次迭代会创建一个分类器的副本,用训练器对这个副本进行训练,然后测试集进行测试
  • 最后预测出准确率,输出正确的比例

In [16]:

代码语言:javascript
复制
# K折交叉验证
from sklearn.model_selection import StratifiedKFold
# 用于生成分类器的副本
from sklearn.base import clone

# 实例化对象
k_folds = StratifiedKFold(
    n_splits = 3,  # 3折
    shuffle=True,  # add   一定要设置shuffle才能保证random_state生效
    random_state=42
)

# 每个折叠由StratifiedKFold执行分层抽样
for train_index, test_index in k_folds.split(X_train, y_train_0):
    # 分类器的副本
    clone_c = clone(sgd_c)

    X_train_folds = X_train[train_index]  # 训练集的索引号
    y_train_folds = y_train_0[train_index]

    X_test_fold = X_train[test_index]  # 测试集的索引号
    y_test_fold = y_train_0[test_index]

    clone_c.fit(X_train_folds, y_train_folds)  # 模型训练
    y_pred = clone_c.predict(X_test_fold)  # 预测

    n_correct = sum(y_pred == y_test_fold)  # 预测准确的数量

    print(n_correct / len(y_pred))  # 预测准确的比例

运行的结果如下:

代码语言:javascript
复制
[0.09875 0.09875 0.09875 ... 0.90125 0.90125 0.90125]
[0.0987 0.0987 0.0987 ... 0.9013 0.9013 0.9013]
[0.0987 0.0987 0.0987 ... 0.9013 0.9013 0.9013]

scikit_learn的交叉验证

使用cross_val_score来评估分类器:

In [17]:

代码语言:javascript
复制
# 评估分类器的效果
from sklearn.model_selection import cross_val_score

cross_val_score(sgd_c,  # 模型
                X_train,  # 数据集
                y_train_0,
                cv=3,  # 3折
                scoring="accuracy"  # 准确率
               )

# 结果
array([0.98015, 0.95615, 0.9706 ])

可以看到准确率已经达到了95%以上,效果是相当的可观

自定义一个“非0”的简易分类器,看看效果:

In [18]:

代码语言:javascript
复制
from sklearn.base import BaseEstimator  # 基分类器

class Never0Classifier(BaseEstimator):
    def fit(self, X, y=None):
        return self

    def predict(self, X):
        return np.zeros((len(X), 1), dtype=bool)

In [19]:

代码语言:javascript
复制
never_0_clf = Never0Classifier()

cross_val_score(
    never_0_clf,  # 模型
    X_train,   # 训练集样本
    y_train_0,  # 训练集标签
    cv=3,  # 折数
    scoring="accuracy"
    )

Out[19]:

代码语言:javascript
复制
array([0.70385, 1.     , 1.     ])

In [20]:

统计数据中每个字出现的次数:

代码语言:javascript
复制
pd.DataFrame(y).value_counts()

Out[20]:

代码语言:javascript
复制
1    7877
7    7293
3    7141
2    6990
9    6958
0    6903
6    6876
8    6825
4    6824
5    6313
dtype: int64

In [21]:

代码语言:javascript
复制
6903 / 70000

Out[21]:

下面显示大约有10%的概率是0这个数字

代码语言:javascript
复制
0.09861428571428571

In [22]:

代码语言:javascript
复制
(0.70385 + 1 + 1) / 3

Out[22]:

代码语言:javascript
复制
0.9012833333333333

可以看到判断“非0”准确率基本在90%左右,因为只有大约10%的样本是属于数字0。

所以如果猜测一张图片是非0,大约90%的概率是正确的。

性能测量2-混淆矩阵

预测结果

评估分类器性能更好的方法是混淆矩阵,总体思路是统计A类别实例被划分成B类别的次数

混淆矩阵是通过预测值和真实目标值来进行比较的。

cross_val_predict函数返回的是每个折叠的预测结果,而不是评估分数

In [23]:

代码语言:javascript
复制
from sklearn.model_selection import cross_val_predict

y_train_pred = cross_val_predict(
    sgd_c,  # 模型
    X_train,  # 特征训练集
    y_train_0,  # 标签训练集
    cv=3  # 3折
)

y_train_pred

Out[23]:

代码语言:javascript
复制
array([ True,  True,  True, ..., False, False, False])

混淆矩阵

In [24]:

代码语言:javascript
复制
# 导入混淆矩阵
from sklearn.metrics import confusion_matrix

confusion_matrix(y_train_0, y_train_pred)

Out[24]:

代码语言:javascript
复制
array([[52482,  1595],
       [  267,  5656]])

混淆矩阵中:行表示实际类别,列表示预测类别

  • 第一行表示“非0”:52482张被正确地分为“非0”(真负类),有1595张被错误的分成了“0”(假负类)
  • 第二行表示“0”:267被错误地分为“非0”(假正类),有5656张被正确地分成了“0”(真正类)

In [25]:

代码语言:javascript
复制
# 假设一个完美的分类器:只存在真正类和真负类,它的值存在于对角线上

y_train_perfect_predictions = y_train_0

confusion_matrix(y_train_0, y_train_perfect_predictions)

Out[25]:

代码语言:javascript
复制
array([[54077,     0],
       [    0,  5923]])

精度和召回率

精度=\frac{TP}{TP+FP}

召回率的公式为:

召回率 = \frac {TP}{TP+FN}

混淆矩阵显示的内容:

  • 左上:真负
  • 右上:假正
  • 左下:假负
  • 右下:真正

精度:正类预测的准确率

召回率(灵敏度或真正类率):分类器正确检测到正类实例的比例

计算精度和召回率

In [26]:

代码语言:javascript
复制
from sklearn.metrics import precision_score, recall_score

precision_score(y_train_0, y_train_pred)  # 精度

Out[26]:

代码语言:javascript
复制
0.78003034064267

In [27]:

代码语言:javascript
复制
recall_score(y_train_0, y_train_pred)  # 召回率

Out[27]:

代码语言:javascript
复制
0.9549214924869154

F_1系数

F_1系数是精度和召回率的谐波平均值。只有当召回率和精度都很高的时候,分类器才会得到较高的F_1分数

𝐹1=21精度+1召回率(3)(3)F1=21精度+1召回率

In [28]:

代码语言:javascript
复制
from sklearn.metrics import f1_score

f1_score(y_train_0, y_train_pred)

Out[28]:

代码语言:javascript
复制
0.8586609989373006

精度/召回率权衡

精度和召回率通常是一对”抗体“,我们一般不可能同时增加精度又减少召回率,反之亦然,这就现象叫做精度/召回率权衡

In [29]:

代码语言:javascript
复制
# 使用decision_function

y_scores = sgd_c.decision_function([one_digit])
y_scores

Out[29]:

代码语言:javascript
复制
array([24816.66593936])

In [30]:

代码语言:javascript
复制
threshold = 0  # 设置阈值
y_digit_pred = y_scores > threshold
y_digit_pred

Out[30]:

代码语言:javascript
复制
array([ True])

In [31]:

代码语言:javascript
复制
# 提升阈值

threshold = 100000
y_digit_pred = y_scores > threshold
y_digit_pred

Out[31]:

代码语言:javascript
复制
array([False])

如何使用阈值

  1. 先使用cross_val_predict函数获取训练集中所有实例的分数

In [32]:

代码语言:javascript
复制
y_scores = cross_val_predict(
    sgd_c,
    X_train,
    y_train_0.ravel(),  # 原文 y_train_0
    cv=3,
    method="decision_function")

y_scores

Out[32]:

代码语言:javascript
复制
array([ 51616.39393745,  27082.28092103,  20211.29278048, ...,
       -23195.59964776, -21022.63597851, -18702.17990507])

有了这些分数就可以计算精度和召回率:

In [33]:

代码语言:javascript
复制
from sklearn.metrics import precision_recall_curve

precisions, recalls, thresholds = precision_recall_curve(y_train_0, y_scores)

In [34]:

代码语言:javascript
复制
precisions  # 精度

Out[34]:

代码语言:javascript
复制
array([0.10266944, 0.10265389, 0.10265566, ..., 1.        , 1.        ,
       1.        ])

In [35]:

代码语言:javascript
复制
recalls   # 召回率

Out[35]:

代码语言:javascript
复制
array([1.00000000e+00, 9.99831167e-01, 9.99831167e-01, ...,
       3.37666723e-04, 1.68833361e-04, 0.00000000e+00])

In [36]:

代码语言:javascript
复制
thresholds  # 阈值

Out[36]:

代码语言:javascript
复制
array([-86393.49001095, -86375.60229796, -86374.22313529, ...,
        92555.12952489,  93570.30614671,  96529.58216984])

绘制精度和召回率曲线

In [37]:

代码语言:javascript
复制
def figure_precision_recall(precisions, recalls, thresholds):
    plt.plot(thresholds, precisions[:-1],"b--",label="Precision")  # 精度-蓝色
    plt.plot(thresholds, recalls[:-1],"g-",label="Recall")  # 召回率-绿色
    plt.legend(loc="center right", fontsize=12)
    plt.xlabel("Threshold", fontsize=16)
    plt.grid(True)


figure_precision_recall(precisions, recalls, thresholds)
plt.show()

直接绘制精度和召回率的曲线图:

代码语言:javascript
复制
# 精度-召回率

plt.plot(recalls[:-1], precisions[:-1],"b--")
plt.legend(loc="center right", fontsize=12)
plt.xlabel("Threshold", fontsize=16)
plt.grid(True)

现在我们将精度设置成90%,通过np.argmax()函数来获取最大值的第一个索引,即表示第一个True的值:

In [39]:

代码语言:javascript
复制
threshold_90_precision = thresholds[np.argmax(precisions >= 0.9)]
threshold_90_precision

Out[39]:

代码语言:javascript
复制
9075.648564157285

In [40]:

代码语言:javascript
复制
y_train_pred_90 = (y_scores >= threshold_90_precision)
y_train_pred_90

Out[40]:

代码语言:javascript
复制
array([ True,  True,  True, ..., False, False, False])

In [41]:

代码语言:javascript
复制
# 再次查看精度和召回率

precision_score(y_train_0, y_train_pred_90)

Out[41]:

代码语言:javascript
复制
0.9001007387508395

In [42]:

代码语言:javascript
复制
recall_score(y_train_0, y_train_pred_90)

Out[42]:

代码语言:javascript
复制
0.9051156508526085

性能测量3-ROC曲线

绘制ROC

还有一种经常和二元分类器一起使用的工具,叫做受试者工作特征曲线ROC。

绘制的是真正类率(召回率的别称)和假正类率(FPR)。FPR是被错误分为正类的负类实例比率,等于1减去真负类率(TNR)

TNR是被正确地分为负类的负类实例比率,也称之为特异度

ROC绘制的是灵敏度和(1-特异度)的关系图

In [43]:

代码语言:javascript
复制
# 1、计算TPR、FPR

from sklearn.metrics import roc_curve

fpr, tpr, thresholds = roc_curve(y_train_0, y_scores)

In [44]:

代码语言:javascript
复制
def plot_roc_curve(fpr,tpr,label=None):

    plt.plot(fpr, tpr, linewidth=2,label=label)
    plt.plot([0,1], [0,1], "k--")
    plt.legend(loc="center right", fontsize=12)
    plt.xlabel("FPR", fontsize=16)
    plt.ylabel("TPR", fontsize=16)
    plt.grid(True)

plot_roc_curve(fpr,tpr)
plt.show()

AUC面积

auc就是上面ROC曲线的线下面积。完美的分类器ROC_AUC等于1;纯随机分类器的ROC_AUC等于0.5

In [45]:

代码语言:javascript
复制
from sklearn.metrics import roc_auc_score

roc_auc_score(y_train_0, y_scores)

Out[45]:

代码语言:javascript
复制
0.9910680354987216

ROC曲线和精度/召回率(PR)曲线非常类似,选择经验:当正类非常少见或者我们更加关注假正类而不是假负类,应该选择PR曲线,否则选择ROC曲线

对比随机森林分类器

报错解决方案:https://stackoverflow.com/questions/63506197/method-predict-proba-for-cross-val-predict-return-index-1-is-out-of-bounds-fo

报错:index 1 is out of bounds for axis 1 with size 1

In [46]:

代码语言:javascript
复制
X_train.shape

Out[46]:

代码语言:javascript
复制
(60000, 784)

In [47]:

代码语言:javascript
复制
# 解决方案
y_train_0 = y_train_0.reshape(X_train.shape[0], )
y_train_0

Out[47]:

代码语言:javascript
复制
array([ True,  True,  True, ..., False, False, False])

In [48]:

代码语言:javascript
复制
from sklearn.ensemble import RandomForestClassifier

forest_clf = RandomForestClassifier(random_state=42)
y_probas_forest = cross_val_predict(forest_clf,
                                    X_train,
                                    y_train_0,
                                    cv=3,
                                    method="predict_proba")
y_probas_forest

Out[48]:

代码语言:javascript
复制
array([[0.  , 1.  ],
       [0.04, 0.96],
       [0.15, 0.85],
       ...,
       [0.93, 0.07],
       [0.97, 0.03],
       [0.96, 0.04]])

使用roc_curve函数来提供分类的概率:

In [49]:

代码语言:javascript
复制
y_scores_forest = y_probas_forest[:,1]

fpr_rf, tpr_rf, thresholds_rf = roc_curve(y_train_0, y_scores_forest)

In [50]:

代码语言:javascript
复制
plt.plot(fpr, tpr, "b:", label="SGD")
plot_roc_curve(fpr_rf,tpr_rf,"Random Forest")
plt.legend(loc="lower right")

plt.show()

现在我们重新查看ROC-AUC值、精度和召回率,发现都得到了提升:

In [51]:

代码语言:javascript
复制
roc_auc_score(y_train_0,y_scores_forest)  # ROC-AUC值

Out[51]:

代码语言:javascript
复制
0.9975104189747056

In [52]:

代码语言:javascript
复制
precision_score(y_train_0,y_train_pred)  # 精度

Out[52]:

代码语言:javascript
复制
0.78003034064267

In [53]:

代码语言:javascript
复制
recall_score(y_train_0,y_train_pred)  # 召回率

Out[53]:

代码语言:javascript
复制
0.9549214924869154

总结

本文从公开的MNIST数据出发,通过SGD建立一个二元分类器,同时利用交叉验证来评估我们的分类器,以及使用不同的指标(精度、召回率、精度/召回率平衡)、ROC曲线等来比较SGD和RandomForestClassifier不同的模型。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-6-8,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • MNIST数据集:二分类问题
  • 导入数据
  • 创建特征和标签
  • 显示一张图片
    • 标签类型转换
    • 创建训练集和测试集
    • 二元分类器
      • 随机梯度下降分类器SGD
        • 结果验证
        • 性能测量1-交叉验证
          • 自定义交差验证(优化)
            • scikit_learn的交叉验证
            • 性能测量2-混淆矩阵
              • 预测结果
                • 混淆矩阵
                  • 精度和召回率
                    • 计算精度和召回率
                      • F_1系数
                        • 精度/召回率权衡
                          • 如何使用阈值
                            • 绘制精度和召回率曲线
                            • 性能测量3-ROC曲线
                              • 绘制ROC
                                • AUC面积
                                • 对比随机森林分类器
                                • 总结
                                相关产品与服务
                                腾讯云服务器利旧
                                云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
                                领券
                                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档