手写体识别

本文本文所述内容,参考了机器学习实战:基于Scikit-Learn和TensorFlow一书。
修正了书籍中的代码错误,文章中代码从上到下,组成一个完整的可运行的项目。

获取MNIST

在github上下载:MNIST训练集

from sklearn.datasets import fetch_mldata
mnist=fetch_mldata('MNIST original',data_home='.')    # 下载文件放在./mldata下
print(mnist)
### DESCR为数据集的描述,data包含的每个实例,target为标记数组
{'DESCR': 'mldata.org dataset: mnist-original', 'COL_NAMES': ['label', 'data'], 'target': array([0., 0., 0., ..., 9., 9., 9.]), 'data': array([[0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       ...,
       [0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0]], dtype=uint8)}

查看数据集:

X, y = mnist["data"], mnist["target"]
print(X.shape, y.shape)   # (70000, 784) (70000,)
# 查看一张图片
import matplotlib.pyplot as plt
import  matplotlib
some_digit=X[36000]       # 数字5
some_digit_image=some_digit.reshape(28,28)
plt.imshow(some_digit_image,cmap=matplotlib.cm.binary,
           interpolation="nearest")
plt.axis("off")
plt.show()

实际上,MNIST的训练集有6万张和测试1万张。

X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]

对训练数据集洗牌(有利于交叉验证):

import numpy as np
shuffle_index = np.random.permutation(60000)
X_train, y_train = X_train[shuffle_index], y_train[shuffle_index]

训练一个二元分类器

建立数字5的分类器:

# 目标向量
y_train_5=(y_train==5)
y_test_5=(y_test==5)

from sklearn.linear_model import  SGDClassifier

sgd_clf=SGDClassifier(random_state=42)
sgd_clf.fit(X_train,y_train_5)
print(sgd_clf.predict([some_digit]))   #True
  1. 使用交叉验证测量精度
    可以自己实现:
from sklearn.model_selection import StratifiedKFold
from sklearn.base import clone

skfolds = StratifiedKFold(n_splits=3, random_state=42)   # 3折,其中一折用于测试

for train_index, test_index in skfolds.split(X_train, y_train_5):
    clone_clf = clone(sgd_clf)
    X_train_folds = X_train[train_index]
    y_train_folds = (y_train_5[train_index])
    X_test_fold = X_train[test_index]
    y_test_fold = (y_train_5[test_index])

    clone_clf.fit(X_train_folds, y_train_folds)
    y_pred = clone_clf.predict(X_test_fold)
    n_correct = sum(y_pred == y_test_fold)
    print(n_correct / len(y_pred))

或者:

from sklearn.model_selection import cross_val_score

print(cross_val_score(sgd_clf, X_train, y_train_5, cv=3, scoring='accuracy'))
# [0.95945 0.967   0.95605]

但是准确率不能准确衡量分类器的性能,比如讲所有的数字都识别为非5,则准确率可达到90%!

  1. 混淆矩阵
    思路是统计A实例被分成B类别的次数。

from sklearn.model_selection import cross_val_predict
from sklearn.metrics import  confusion_matrix
y_train_pred=cross_val_predict(sgd_clf,X_train,y_train_5,cv=3)  # 返回每个折叠的预测
print(confusion_matrix(y_train_5,y_train_pred))
[[54045   534]        真负例|假正例
 [ 1866  3555]]           假负例|真正例

精度:
a c c = T P T P + F P {\rm{acc = }}{{{\rm{TP}}} \over {TP + FP}} acc=TP+FPTP
灵敏度(召回率):
r e c a l l = T P T P + F N {\rm{recall = }}{{{\rm{TP}}} \over {TP + FN}} recall=TP+FNTP
混淆矩阵图解:
混淆矩阵

from sklearn.metrics import  precision_score,recall_score
print(precision_score(y_train_5,y_train_pred))  # 0.7953699875981811
print(recall_score(y_train_5,y_train_pred))  # 0.709832134292566

F1分数:精度和召回率的谐波平均值,对那些具有相近的精度和召回率的分类器更为有利,不能同时增加精度并减少召回率。
F 1 = 2 1 a c c + 1 r e c a l l = T P T P + F N + F P 2 {\rm{F1 = }}{2 \over {{1 \over {acc}}{\rm{ + }}{1 \over {{\rm{recall}}}}}} = {{TP} \over {TP + {{FN + FP} \over 2}}} F1=acc1+recall12=TP+2FN+FPTP

from sklearn.metrics import f1_score
print(f1_score(y_train_5, y_train_pred))  # 0.7640830156713256

精度和召回率在scikit-learn中,可以访问用于预测的决策分数,不是调用predict()方法,而是使用decision_function方法,然后用任意阈值预测:

y_score = sgd_clf.decision_function([some_digit])
threshold = 0
y_some_digit_pred = (y_score > threshold)    # SGD中predict默认阈值为0

如何选取合适阈值呢?

y_scores = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3, method='decision_function')    # 训练集中实例分数

from sklearn.metrics import precision_recall_curve
precisions, recalls, thresholds = precision_recall_curve(y_train_5, y_scores)

def plot_precision_recall_threshold(precisions, recalls, thresholds):
    plt.plot(thresholds, precisions[:-1],"b--", label="Precision")
    plt.plot(thresholds, recalls[:-1], "g-", label="Recall")
    plt.xlabel("Threshold")
    plt.legend(loc="upper left")
    plt.ylim([0, 1])

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

精度和召回率还可以绘制精度和召回率的图:

plt.plot(recalls,precisions,"r-")
plt.xlabel("Recall")
plt.ylabel("Precision")
plt.ylim([0,1])
plt.xlim([0,1])
plt.legend()
plt.show()

精度/召回率获取90%精度的分类器,结合上图设置:

y_train_pred_90=(y_scores>100000)   # 大约
print(precision_score(y_train_5,y_train_pred_90))  # 0.80855569673867
print(recall_score(y_train_5,y_train_pred_90))  # 0.7042980999815532
  1. ROC曲线
    绘制的是真正例率(召回率TPR)和假正例率(FPR)的曲线(1-TNR)。FPR为错误分为正例的负实例的比率。
    T P R = T P T P + F N TPR{\rm{ = }}{{{\rm{TP}}} \over {TP + FN}} TPR=TP+FNTP
    F P R = F P F P + T N FPR{\rm{ = }}{{FP} \over {FP + TN}} FPR=FP+TNFP
    T N R = T N F P + T N TNR{\rm{ = }}{{TN} \over {FP + TN}} TNR=FP+TNTN
    F N R = F N T P + F N FNR{\rm{ = }}{{FN} \over {TP + FN}} FNR=TP+FNFN
    ROC曲线召回率(TPR)越高,分类器产生的假正例(FPR)就越多。虚线表示纯随机分类器的ROC曲线;一个优秀的分类器应该离这条线越远越好(向左上角)。
    还有一种比较分类器的方法是测量曲线下面积(AUC)。完美的分类器的ROC AUC等于1,而纯随机的分类器ROC AUC等于0.5.。
from sklearn.metrics import roc_auc_score
print(roc_auc_score(y_train_5,y_scores))   # 0.9310184686519247

如何选择这两种曲线呢?当正例非常少见或者更关注假正例时,应该选择PR曲线,反之则是ROC曲线。
使用RandomForestClassifier分类器:

from sklearn.ensemble import  RandomForestClassifier

forest_clf=RandomForestClassifier(random_state=42)
y_probas_forest=cross_val_predict(forest_clf,X_train,y_train_5,method='predict_proba')
y_score_forest=y_probas_forest[:,1]   # 正例概率做分数值
fpr_forest,tpr_forest,thresholds_forest=roc_curve(y_train_5,y_score_forest)

plt.plot(fpr,tpr,"b:",label="SGD")
plot_roc_curve(fpr_forest,tpr_forest,"Random Forest")
plt.legend()
plt.show()
roc_auc_score(y_train_5,y_score_forest)

SGD/Random ForestRandomForestClassifier的ROC曲线看起来比SGDClassifier好很多。

多分类识别器

一对多(OvA)策略:要创建一个系统将数字图片分为10类(从0到9),一种方法是训练10个二元分类器,每个数字一个(0-检测器、1-检测器、2-检测器,等等,以此类推)。对图片检测分类时,获取每个分类器的决策分数,哪个分类器的给分高,就将其分为哪个类。
一对一(OvO)策略:为每一对数字训练一个二元分类器:一个用于区分0和1,一个区分0和2,一个区分1和2,以此类推。如果存在N个类别,那么这需要训练N×(N-1)÷2个分类器。识别时,则要看哪个类别获胜最多。OvO的主要优点在于,每个分类器只需要用到部分训练集对其必须区分的两个类别进行训练。
对大多数二元分类器,OvA策略是更好的选择。
Scikit-Learn可以检测到你尝试使用二元分类算法进行多类别分类任务,它会自动运行OvA(SVM分类器除外,它会使用OvO)。

sgd_clf.fit(X_train,y_train)
some_digit_score=sgd_clf.decision_function([some_digit])
print(some_digit_score)
print(np.argmax(some_digit_score))
print(sgd_clf.classes_)
print(sgd_clf.predict([some_digit]))
###
[[-157107.21433028 -368055.89268944 -408671.76539565 -354080.69827286
  -333314.05573146  -82633.37714596 -600499.19277988 -400148.79188159
  -410815.16116092 -669079.10702799]]
5
[0. 1. 2. 3. 4. 5. 6. 7. 8. 9.]
[5.]

还可以强制使用一对一或者一对多策略。

from  sklearn.multiclass import  OneVsOneClassifier
ovo_clf=OneVsOneClassifier(SGDClassifier(random_state=42))
ovo_clf.fit(X_train,y_train)
print(ovo_clf.predict([some_digit]))   # 5
print(len(ovo_clf.estimators_))    # 45

随机森林分类器可以直接将实例分为多个类别:

forest_clf.fit(X_train,y_train)
forest_clf.predict([some_digit])
print(forest_clf.predict_proba([some_digit]))      # [[0.  0.  0.  0.2 0.  0.8 0.  0.  0.  0. ]]  分为每个类别的概率
print(cross_val_score(sgd_clf,X_train,y_train,cv=3,scoring='accuracy'))  # [0.8489802  0.83094155 0.84097615]

可以对输入进行简单缩放:

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
x_train_scaled = scaler.fit_transform(X_train.astype(np.float64))
print(cross_val_score(sgd_clf, x_train_scaled, y_train, cv=3, scoring='accuracy'))# [0.90831834 0.91104555 0.9099865 ]

错误分析

  1. 查看混淆矩阵
    查看混淆矩阵的图像表示:
y_train_pred=cross_val_predict(sgd_clf,x_train_scaled,y_train,cv=3)
conf_mx=confusion_matrix(y_train,y_train_pred)
plt.matshow(conf_mx,cmap=plt.cm.gray)
plt.show()

混淆矩阵比较错误率:

row_sums=conf_mx.sum(axis=1,keepdims=True)
norm_conf_max=conf_mx/row_sums

np.fill_diagonal(norm_conf_max,0)     # 用0填充对角线
plt.matshow(norm_conf_max,cmap=plt.cm.gray)
plt.show()

混淆矩阵错误每行代表实际类别,列表示预测类别。第8列和第9列整体看起来非常亮,说明有许多图片被错误地分类为数字8或数字9了。同样,类别8和类别9的行看起来也偏亮,说明数字8和数字9经常会跟其他数字混淆。相反,一些行很暗,比如行1,这意味着大多数数字1都被正确地分类。注意,错误不是完全对称的,比如,数字5被错误分类为字8的数量比数字8被错误分类为数字5的数量要更多。
从上图可以看出,我们可以改进数字8和9的分类,以及修正数字3和5的混淆上。

多标签分类

输出对个二元标签的分类系统。

from sklearn.neighbors import  KNeighborsClassifier

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)
print(knn_clf.predict([some_digit]))     #  [[False  True]]
y_train_knn_pred=cross_val_predict(knn_clf,X_train,y_train,cv=3)
print(f1_score(y_train,y_train_knn_pred,average='macro'))    # 平均f1分数,假定所有标签都同等重要
# 可以设置置average="weighted",每个标签设置一个等于其之身的权重

多输出分类

多标签分类的泛化,其标签也可以是多种类别的。假设一个图片清晰系统,对图片中的每个像素进行操作:

mport numpy.random as rnd
train_noise = rnd.randint(0, 100, (len(X_train), 784))
test_noise=rnd.randint(0,100,(len(X_test),784))

X_train_mod=X_train+train_noise
X_test_mod=X_test+test_noise
y_train_mod=X_train
y_test_mod=X_test

knn_clf.fit(X_train_mod,y_train_mod)
clean_digit=knn_clf.predict([X_test_mod[some_digit]])
plt.plot(clean_digit)
plt.show()
  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值