机器学习(三)

分类

Posted by Gavin on March 28, 2019

明月皎皎照我床

星汉西流夜未央

概述

最常见的监督学习任务包括回归(预测值)和分类任务(预测类),这次将尝试一个分类任务。


实验流程

实验数据

将使用 MNIST 数据集,这是一组美国高中生和人口调查局员工手写的70000个数字的图片。此数据集的广泛使用性,基本可以被誉为机器学习届的“Hello World”

  1. 下载实验数据

     from sklearn.datasets import fetch_mldata
    	
     mnist = fetch_mldata('MNIST original')
    

    发现无法下载,因此只能选择手动下载。手动下载数据集到~/scikit_learn_data/mldata文件夹下,再次运行上面的代码,成功导入数据集

  2. 预览数据

    Scikit-Learn加载的数据集通常具有字典结构,其中:
    • DESCR:描述数据集
    • data:数据,每个实例为一行,每个特征为一列
    • target:一个带标记的数组

      共有70000张图片,每张图片有784个特征。
      PS:因为图片是28x28像素,每个特征代表了一个像素点的强度,从0(白色)到255(黑色)
      预览其中一张图片
     some_digit = X[34500]
     some_digit_image = some_digit.reshape(28, 28)
     plt.imshow(some_digit_image, cmap = matplotlib.cm.binary, interpolation='nearest')
     plt.axis('off')
     plt.savefig('digit.png')
    


    貌似是5,查看标签,确实是5

  3. 划分数据集
    事实上,MNIST数据集已经被划分过,前60000张图像是训练集,最后10000张是测试集,为了更具有一般性,我在此使用随机数对数据集顺序进行洗牌

    X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]
    shuffle_index = np.random.permutation(60000)
    X_train, y_train = X_train[shuffle_index], y_train[shuffle_index]
    

    PS:有时候对数据集进行洗牌是不合适的,比如时间序列数据

二元分类器

  1. 首先简化问题为只识别一个数字——5
    它只能判断两个类别 5非5

     y_train_5 = (y_train == 5)
     y_test_5 = (y_test == 5)
    	
     sgd_clf = SGDClassifier(random_state = 42)
     sgd_clf.fit(X_train,y_train_5)
    

    训练结束,检测一个图像试试

    预测正确

  2. 性能考核
    • 使用交叉验证测量精度
        cross_val_score(sgd_clf, X_train, y_train_5, cv = 3, scoring = 'accuracy')
      


      似乎准确率高达95%
      但是即使你将每张图都分类成 非5 ,准确率都可以高达90%,因为数据集中90%的数据都不是5,90%的时候,武断地判断所有数字都不是5,都会正确,因此准确率一般无法成为分类器的主要性能指标,尤其是处理偏倚数据集时

    • 混淆矩阵 评估分类器更好的指标是混淆矩阵,即统计A类实例被预测为B类的次数。要计算混淆矩阵,需要先有一组预测结果才能与目标进行比较

      y_train_pred = cross_val_predict(sgd_clf, X_train, y_train_5, cv = 3)
      confusion_matrix(y_train_5, y_train_pred)
      

      得到如下混淆矩阵

        预测非5 预测5
      非5(负类) 53558(真负类) 1021(假正类)
      5(正类) 1307(假负类) 4114(真正类)

      一个完美的分类器的结果是假正类、假负类不存在,其混淆矩阵只会在其对角线上存在非0值
      混淆矩阵参数:

      • 精度(正类预测的准确率)

        TP:真正类的数量
        FP:假正类的数量
      • 召回率

        也称灵敏度,分类器正确检测到的正类实例的比率
  3. 计算精度与召回率

     precision_score(y_train_5, y_train_pred)
     recall_score(y_train_5, y_train_pred)
    


    说明此分类器预测结果为5时,80%是对的,但它也仅仅找出了75%的数字5
    我们可以将精度和召回率组合成一个单一的指标,成为 F1分数 ,它是精度和召回率的谐波平均值
    PS:谐波平均值会给予较低的值更高的权重,因此只有召回率和精度都很高时,F1分数才会较高

  4. 精度/召回率权衡
    一般来说,我们无法既提高精度,又增加召回率
    可以通过调整判定阈值来提高进度或召回率

    y_scores = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3, method='decision_function')
    precisions, recalls, thresholds = precision_recall_curve(y_train_5, y_scores)
    plot_precision_recall_vs_threshold(precisions, recalls, thresholds, 'threshold_vs_pre_rec.png')
    


    或者绘制精度-召回率函数图进行权衡

     plt.plot(precisions, recalls)
     plt.xlabel('Precision')
     plt.ylabel('Recall')
     plt.savefig('P_vs_R.png')
    


    如果我想要一个精度高达90%的分类器,则可以通过阈值锚定

     y_train_pred_90 = (y_scores > 70000)
     precision_score(y_train_5, y_train_pred_90)
     recall_score(y_train_5, y_train_pred_90)
    


    但召回率实在不敢恭维

  5. ROC曲线
    受试者工作特征曲线是另一个经常与二元分类器一起使用的工具,其绘制的是真正类率(TPR)与假正类率(FPR)的曲线

    fpr, tpr, thresholds = roc_curve(y_train_5, y_scores)
    plt.plot(fpr, tpr, linewidth=2)
    plt.plot([0,1],[0,1],'k--')
    plt.axis([0, 1, 0, 1])
    plt.xlabel('Flase Positive Rate')
    plt.ylabel('True Positive Rate')
    plt.savefig('Roc_sdg.png')
    

  6. 引入随机森林
    因为随机森林分类器的工作方式与SGD分类器工作方式不同,因此其没有decision_function()方法,但是有predict_proba()方法。它会返回一个数组,每行是一个实例,每列是一个类别,每个值是此实例属于此类的概率。绘制ROC曲线需要分数值,一般方法是直接使用概率充当分数值。

    forest_clf = RandomForestClassifier(random_state=42)
    y_probas_forest = cross_val_predict(forest_clf, X_train, y_train_5, cv=3, method='predict_proba')
    y_scores_forest = y_probas_forest[:,1]#TP rate
    fpr_forest, tpr_forest, thresholds_forest = roc_curve(y_train_5, y_scores_forest)
    plt.plot(fpr_forest, tpr_forest, 'b:', label='Random Forest')
    plt.plot(fpr, tpr, label='SGD')
    plt.legend(loc='bottom right')
    plt.savefig('sgd_ranfor_roc.png')
    


    貌似随机森林分类器表现更好,再检查一下精度和召回率

    效果的确更佳

PS:P/R曲线与ROC曲线貌似差不多,那么应该使用哪种曲线呢?经验法则是——当正类非常少或者我们更加关注假正类而不是假负类时,应该选择P/R曲线,反之ROC曲线

多类别分类器

有些机器学习算法可以直接处理多个类别的分类(如随机森林分类器或朴素贝叶斯分类器),也有一些是严格的二元分类器(如支持向量机分类器或线性分类器)。但是有多种策略使用二元分类器来完成多类别分类的任务:

  • OvA(一对多)
    比如MNIST的例子,创建10个单独的数字分类器,你想要检测一张图片是什么数字时,获取每个分类器的分数,将其归类于最高的那个分类器结果
  • OvO(一对一)
    每对数字两两构建二元分类器,MNIST的例子中要运行(0-1分类,0-2分类,1-2分类……)等45个分类器,最后看哪个类别数目最多,哪个类别获胜

PS:有些算法(SVM)在数据规模扩大时表现糟糕,对于此类算法,OvO是一个优先地选择,但一般来说OvA的策略更好

  1. 训练多类别分类器(OvA)

    sgd_clf.fit(X_train, y_train)
    sgd_clf.predict([some_digit])
    some_digit_scores = sgd_clf.decision_function([some_digit])
    np.argmax(some_digit_scores)
    sgd_clf.classes_
    sgd_clf.classes[5]
    


    如图,此时SGD分类器给出10个打分,其中最高的那个就是最终归类

  2. 强制训练OvO分类器

    ovo_clf = OneVsOneClassifier(SGDClassifier(random_state=42))
    ovo_clf.fit(X_train, y_train)
    ovo_clf.predict([some_digit])
    len(ovo_clf.estimators_)
    


    如图训练了45个分类器

  3. 随机森林分类器

    forest_clf.fit(X_train, y_train)
    forest_clf.predict([some_digit])
    forest_clf.predict_proba([some_digit])
    cross_val_score(forest_clf, X_train, y_train, cv=3, scoring='accuracy')
    


    分类器十分自信
    再进行一点简单的缩放

    scaler = StandardScaler()
    X_train_scaled = scaler.fit_transform(X_train.astype(np.float64))
    cross_val_score(forest_clf, X_train_scaled, y_train, cv=3, scoring='accuracy')
    


    每次折叠准确率高达94%

  4. 错误分析 查看混淆矩阵
    y_train_pred = cross_val_predict(sgd_clf, X_train_scaled, y_train, cv=3)
    conf_mx = confusion_matrix(y_train, y_train_pred)
    


    数字有点多,将其可视化

    f,ax = plt.subplots(figsize=(25, 25))
    sns.heatmap(conf_mx, annot = True, linewidth = .5, fmt = ".3f",ax = ax)
    plt.savefig('conf.png')
    


    看起来不错,但我要进行错误分析,因此需要将混淆矩阵中的每个值除以相应类别的总数以比较错误率,同时用0填充对角线

    row_sums = conf_mx.sum(axis=1, keepdims=True)
    norm_conf_mx = conf_mx / row_sums
    np.fill_diagonal(norm_conf_mx, 0)
    f,ax = plt.subplots(figsize=(25, 25))
    sns.heatmap(norm_conf_mx, annot = True, linewidth = .5, fmt = ".3f",ax = ax)
    plt.savefig('norm_conf.png')
    


    可以清晰看到错误类型了,第8、9两列比较亮,说明8、9会经常与其他数字混淆。而第0、1行很暗,说明0、1基本可以被正确识别。

  5. 对单个错误进行分析

    3和5确实难以区分

多标签分类

目前所有的实例只会被分到一个类别中,但某些情况下,可能希望分类器为每个实例产出多个类别。比如人脸分类器,一张图中可能有多个人脸。

y_train_large = (y_train >= 7)
y_train_odd = (y_train % 2 == 1)
y_multilabel = np.c_[y_train_large,y_train_odd]
knn_clf = KNeighborsClassifier()
knn_clf.fit(X_train, y_multilabel)
knn_clf.predict([some_digit])

这里训练了一个区分一张图是否是大数且是否是奇数的多标签分类器

正确!5不是大数,但5是奇数
评估多标签分类器的方法很多,比如平均F1分数

多输出分类

多输出-多类别分类是多标签分类的泛化,即标签也是有可能有多个种类的,如还原图片的任务

  1. 首先给图片加上噪音

    some_digit_image = X_train_mod[45600].reshape(28,28)
    plt.imshow(some_digit_image, cmap = matplotlib.cm.binary, interpolation='nearest')
    plt.axis('off')
    plt.savefig('noise.png')
    

  2. 训练去噪模型

    knn_clf.fit(X_train_mod, y_train_mod)
    clean_digit = knn_clf.predict([X_train_mod[45600]])
    clean_digit_image = clean_digit.reshape(28,28)
    plt.imshow(clean_digit_image, cmap = matplotlib.cm.binary, interpolation='nearest')
    plt.axis('off')
    plt.savefig('clean.png')
    


    去噪结果不错,此例就是一个典型的多输出分类。这个分类器输出288x288个标签,每个标签有0~255个值


总结

  1. 多类别分类任务可以拆分为单个二元分类器
  2. 不同情况适用不同评估指标
  3. 根据场景选用合适的分类器