MNIST
数据介绍:本章使用MNIST数据集,这是一组由美国高中生和人口调查局员工手写的70000个数字的图片。每张图像都用其代表的数字标记。这个数据集被广为使用,因此也被称作是机器学习领域的“Hello World”:但凡有人想到了一个新的分类算法,都会想看看在MNIST上的执行结果。因此只要是学习机器学习的人,早晚都要面对MNIST。
# 使用sklearn的函数来获取MNIST数据集from sklearn.datasets import fetch_openmlimport numpy as npimport os# to make this notebook's output stable across runsnp.random.seed(42)# To plot pretty figures%matplotlib inlineimport matplotlib as mplimport matplotlib.pyplot as pltmpl.rc('axes', labelsize=14)mpl.rc('xtick', labelsize=12)mpl.rc('ytick', labelsize=12)# 为了显示中文mpl.rcParams['font.sans-serif'] = [u'SimHei']mpl.rcParams['axes.unicode_minus'] = False
# 耗时巨大def sort_by_target(mnist):reorder_train=np.array(sorted([(target,i) for i, target in enumerate(mnist.target[:60000])]))[:,1]reorder_test=np.array(sorted([(target,i) for i, target in enumerate(mnist.target[60000:])]))[:,1]mnist.data[:60000]=mnist.data[reorder_train]mnist.target[:60000]=mnist.target[reorder_train]mnist.data[60000:]=mnist.data[reorder_test+60000]mnist.target[60000:]=mnist.target[reorder_test+60000]
import timea=time.time()mnist=fetch_openml('mnist_784',version=1,cache=True)mnist.target=mnist.target.astype(np.int8)sort_by_target(mnist)b=time.time()print(b-a)
27.996558666229248
mnist["data"], mnist["target"]
(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.]]),array([0, 0, 0, ..., 9, 9, 9], dtype=int8))
mnist.data.shape
(70000, 784)
X,y=mnist["data"],mnist["target"]X.shape
(70000, 784)
y.shape
(70000,)
28*28
784
# 展示图片def plot_digit(data):image = data.reshape(28, 28)plt.imshow(image, cmap = mpl.cm.binary,interpolation="nearest")plt.axis("off")some_digit = X[36000]plot_digit(X[36000].reshape(28,28))
y[36000]
5
# 更好看的图片展示def plot_digits(instances,images_per_row=10,**options):size=28# 每一行有一个image_pre_row=min(len(instances),images_per_row)images=[instances.reshape(size,size) for instances in instances]#有几行n_rows=(len(instances)-1) // image_pre_row+1row_images=[]n_empty=n_rows*image_pre_row-len(instances)images.append(np.zeros((size,size*n_empty)))for row in range(n_rows):# 每一次添加一行rimages=images[row*image_pre_row:(row+1)*image_pre_row]# 对添加的每一行的额图片左右连接row_images.append(np.concatenate(rimages,axis=1))# 对添加的每一列图片 上下连接image=np.concatenate(row_images,axis=0)plt.imshow(image,cmap=mpl.cm.binary,**options)plt.axis("off")
plt.figure(figsize=(9,9))example_images=np.r_[X[:12000:600],X[13000:30600:600],X[30600:60000:590]]plot_digits(example_images,images_per_row=10)plt.show()
接下来,我们需要创建一个测试集,并把其放在一边。
X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]
同样,我们还需要对训练集进行洗牌,这样可以保证交叉验证的时候,所有的折叠都差不多。此外,有些机器学习算法对训练示例的循序敏感,如果连续输入许多相似的实例,可能导致执行的性能不佳。给数据洗牌,正是为了确保这种情况不会发生。
import numpy as npshuffer_index=np.random.permutation(60000)X_train,y_train=X_train[shuffer_index],y_train[shuffer_index]
训练一个二分类器
现在,我们先简化问题,只尝试识别一个数字,比如数字5,那么这个"数字5检测器",就是一个二分类器的例子,它只能区分两个类别:5和非5。先为此分类任务创建目录标量
y_train_5=(y_train==5)y_test_5=(y_test==5)
接着挑选一个分类器并开始训练。一个好的选择是随机梯度下降(SGD)分类器,使用sklearn的SGDClassifier类即可。这个分类器的优势是:能够有效处理非常大型的数据集。这部分是因为SGD独立处理训练实例,一次一个(这也使得SGD非常适合在线学习任务)。
from sklearn.linear_model import SGDClassifiersgd_clf=SGDClassifier(max_iter=5,tol=-np.infty,random_state=42)sgd_clf.fit(X_train,y_train_5)
SGDClassifier(alpha=0.0001, average=False, class_weight=None,early_stopping=False, epsilon=0.1, eta0=0.0, fit_intercept=True,l1_ratio=0.15, learning_rate='optimal', loss='hinge', max_iter=5,n_iter_no_change=5, n_jobs=None, penalty='l2', power_t=0.5,random_state=42, shuffle=True, tol=-inf, validation_fraction=0.1,verbose=0, warm_start=False)
sgd_clf.predict([some_digit])
array([ True])
性能考核
评估分类器比评估回归器要困难很多,因此本章将会用很多篇幅来讨论这个主题,同时也会涉及许多性能考核的方法。
使用交叉验证测量精度
随机交叉验证和分层交叉验证效果对比
from sklearn.model_selection import cross_val_scorecross_val_score(sgd_clf, X_train, y_train_5, cv=3, scoring="accuracy")
array([0.96225, 0.9645 , 0.94765])
# 类似于分层采样,每一折的分布类似from sklearn.model_selection import StratifiedKFoldfrom sklearn.base import cloneskfolds = StratifiedKFold(n_splits=3, random_state=42)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))
0.962250.96450.94765如果你这里出现了警报的话,不用理会,只需耐心等待结果的加载。
我们可以看到两种交叉验证的准确率都达到了95%上下,看起来很神奇,不过在开始激动之前,让我们来看一个蠢笨的分类器,将所有图片都预测为‘非5’
from sklearn.base import BaseEstimator# 随机预测模型class Never5Classifier(BaseEstimator):def fit(self, X, y=None):passdef predict(self, X):return np.zeros((len(X), 1), dtype=bool)never_5_clf = Never5Classifier()cross_val_score(never_5_clf, X_train, y_train_5, cv=3, scoring="accuracy")
array([0.909 , 0.90715, 0.9128 ])
我们可以看到,准确率也超过了90%!这是因为我们只有大约10%的图像是数字5,所以只要猜一张图片不是5,那么有90%的时间都是正确的,简直超过了大预言家。
这说明,准确率通常无法成为分类器的首要性能指标,特别是当我们处理偏斜数据集的时候(也就是某些类别比其他类更加频繁的时候)
混淆矩阵
评估分类器性能的更好的方法是混淆矩阵。总体思路就是统计A类别实例被分成B类别的次数。例如,要想知道分类器将数字3和数字5混淆多少次,只需要通过混淆矩阵的第5行第3列来查看。
要计算混淆矩阵,需要一组预测才能将其与实际目标进行比较。当然可以通过测试集来进行预测,但是现在我们不动它(测试集最好保留到项目的最后,准备启动分类器时再使用)。最为代替,可以使用cross_val_predict()函数:
cross_val_predict
和cross_val_score
不同的是,前者返回预测值,并且是每一次训练的时候,用模型没有见过的数据来预测
from sklearn.model_selection import cross_val_predicty_train_pred = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3)
from sklearn.metrics import confusion_matrixconfusion_matrix(y_train_5, y_train_pred)
array([[53417, 1162],[ 1350, 4071]], dtype=int64)
上面的结果表明:第一行所有’非5’(负类)的图片中,有53417被正确分类(真负类),1162,错误分类成了5(假负类);第二行表示所有’5’(正类)的图片中,有1350错误分类成了非5(假正类),有4071被正确分类成5(真正类).
一个完美的分类器只有真正类和真负类,所以其混淆矩阵只会在其对角线(左上到右下)上有非零值
y_train_perfect_predictions = y_train_5
confusion_matrix(y_train_5, y_train_perfect_predictions)
array([[54579,0],[ 0, 5421]], dtype=int64)
混淆矩阵能提供大量信息,但有时我们可能会希望指标简洁一些。正类预测的准确率是一个有意思的指标,它也称为分类器的精度(如下)。
Precision(精度)=TPTP+FPPrecision(精度)=\frac{TP}{TP+FP}Precision(精度)=TP+FPTP
其中TP是真正类的数量,FP是假正类的数量。
做一个简单的正类预测,并保证它是正确的,就可以得到完美的精度(精度=1/1=100%)
这并没有什么意义,因为分类器会忽略这个正实例之外的所有内容。因此,精度通常会与另一个指标一起使用,这就是召回率,又称为灵敏度或者真正类率(TPR):它是分类器正确检测到正类实例的比率(如下):
Recall(召回率)=TPTP+FNRecall(召回率)=\frac{TP}{TP+FN}Recall(召回率)=TP+FNTP
FN是假负类的数量
精度和召回率
# 使用sklearn的工具度量精度和召回率from sklearn.metrics import precision_score, recall_scoreprecision_score(y_train_5, y_train_pred)
0.7779476399770686
recall_score(y_train_5, y_train_pred)
0.7509684560044272
我们可以看到,这个5-检测器,并不是那么好用,大多时候,它说一张图片为5时,只有77%的概率是准确的,并且也只有75%的5被检测出来了
下面,我们可以将精度和召回率组合成单一的指标,称为F1分数。
F1=21Precision+1Recall=2∗Pre∗RecPre+Rec=TPTP+FN+FP2F_1=\frac{2}{\frac{1}{Precision}+\frac{1}{Recall}}=2*\frac{Pre*Rec}{Pre+Rec}=\frac{TP}{TP+\frac{FN+FP}{2}}F1=Precision1+Recall12=2∗Pre+RecPre∗Rec=TP+2FN+FPTP
要计算F1分数,只需要调用f1_score()即可
from sklearn.metrics import f1_scoref1_score(y_train_5, y_train_pred)
0.7642200112633752
F1分数对那些具有相近的精度和召回率的分类器更为有利。这不一定一直符合预期,因为在某些情况下,我们更关心精度,而另一些情况下,我们可能真正关系的是召回率。
例如:假设训练一个分类器来检测儿童可以放心观看的视频,那么我们可能更青睐那种拦截了好多好视频(低召回率),但是保留下来的视频都是安全(高精度)的分类器,而不是召回率虽高,但是在产品中可能会出现一些非常糟糕的视频分类器(这种情况下,你甚至可能会添加一个人工流水线来检查分类器选出来的视频)。
反过来说,如果你训练一个分类器通过图像监控来检测小偷:你大概可以接受精度只有30%,只要召回率能达到99%。(当然,安保人员会接收到一些错误的警报,但是几乎所有的窃贼都在劫难逃)
遗憾的是,鱼和熊掌不可兼得:我们不能同时增加精度并减少召回率,反之亦然,这称为精度/召回率权衡
精度/召回率权衡
在分类中,对于每个实例,都会计算出一个分值,同时也有一个阈值,大于为正例,小于为负例。通过调节这个阈值,可以调整精度和召回率。
y_scores = sgd_clf.decision_function([some_digit])y_scores
array([150526.40944343])
threshold = 0y_some_digit_pred = (y_scores > threshold)y_some_digit_pred
array([ True])
threshold = 200000y_some_digit_pred = (y_scores > threshold)y_some_digit_pred
array([False])
# 返回决策分数,而不是预测结果y_scores = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3,method="decision_function")y_scores.shape
(60000,)
from sklearn.metrics import precision_recall_curveprecisions, recalls, thresholds = precision_recall_curve(y_train_5, y_scores)
def plot_precision_recall_vs_threshold(precisions, recalls, thresholds):plt.plot(thresholds, precisions[:-1], "b--", label="Precision", linewidth=2)plt.plot(thresholds, recalls[:-1], "g-", label="Recall", linewidth=2)plt.xlabel("Threshold", fontsize=16)plt.title("精度和召回率VS决策阈值", fontsize=16)plt.legend(loc="upper left", fontsize=16)plt.ylim([0, 1])plt.figure(figsize=(8, 4))plot_precision_recall_vs_threshold(precisions, recalls, thresholds)plt.xlim([-700000, 700000])plt.show()
可以看见,随着阈值提高,召回率下降了,也就是说,有真例被判负了,精度上升,也就是说,有部分原本被误判的负例,被丢出去了。
你可以会好奇,为什么精度曲线会比召回率曲线要崎岖一些,原因在于,随着阈值提高,精度也有可能会下降 4/5 => 3/4(虽然总体上升)。另一方面,阈值上升,召回率只会下降。
现在就可以轻松通过选择阈值来实现最佳的精度/召回率权衡了。还有一种找到最好的精度/召回率权衡的方法是直接绘制精度和召回率的函数图。
def plot_precision_vs_recall(precisions, recalls):plt.plot(recalls, precisions, "b-", linewidth=2)plt.xlabel("Recall", fontsize=16)plt.title("精度VS召回率", fontsize=16)plt.ylabel("Precision", fontsize=16)plt.axis([0, 1, 0, 1])plt.figure(figsize=(8, 6))plot_precision_vs_recall(precisions, recalls)plt.show()
可以看见,从80%的召回率往右,精度开始急剧下降。我们可能会尽量在这个陡降之前选择一个精度/召回率权衡–比如召回率60%以上。当然,如何选择取决于你的项目。
假设我们决定瞄准90%的精度目标。通过绘制的第一张图(放大一点),得出需要使用的阈值大概是70000.要进行预测(现在是在训练集上),除了调用分类器的predict方法,也可以使用这段代码:
y_train_pred_90 = (y_scores > 70000)precision_score(y_train_5, y_train_pred_90)
0.8712083540527101
recall_score(y_train_5, y_train_pred_90)
0.6463752075262866
现在我们就有了一个精度接近90%的分类器了,如果有人说,“我们需要99%的精度。”,那么我就要问:“召回率是多少?”
ROC曲线
还有一种经常与二元分类器一起使用的工具,叫做受试者工作特征曲线(简称ROC)。它与精度/召回率曲线非常相似,但绘制的不是精度和召回率,而是真正类率(召回率的另一种称呼)和假正类率(FPR)。FPR是被错误分为正类的负类实例比率。它等于1-真负类率(TNR),后者正是被正确分类为负类的负类实例比率,也称为奇异度。因此ROC曲线绘制的是灵敏度和(1-奇异度)的关系
FPR=FPFP+TNFPR=\frac{FP}{FP+TN}FPR=FP+TNFP
Recall=TPTP+FNRecall=\frac{TP}{TP+FN}Recall=TP+FNTP
# 使用 roc_curve()函数计算多种阈值的TPR和FPRfrom sklearn.metrics import roc_curvefpr, tpr, thresholds = roc_curve(y_train_5, y_scores)def plot_roc_curve(fpr, tpr, label=None):plt.plot(fpr, tpr, linewidth=2, label=label)plt.plot([0, 1], [0, 1], 'k--')plt.axis([0, 1, 0, 1])plt.xlabel('False Positive Rate', fontsize=16)plt.ylabel('True Positive Rate', fontsize=16)plt.figure(figsize=(8, 6))plot_roc_curve(fpr, tpr)plt.show()
这里同样面对一个折中权衡:召回率(TPR)很高,分类器产生的假正类(FPR)就越多。虚线表示纯随机的ROC曲线;一个优秀的分类器(向左上角)。
有一种比较分类器的方式是测量曲线下面积(AUC)。完美的ROC AUC等于1,纯随机分类的ROC AUC等于0.5
from sklearn.metrics import roc_auc_scoreroc_auc_score(y_train_5, y_scores)
0.9562435587387078
ROC曲线和精度/召回率(或PR)曲线非常相似,因此,你可能会问,如何决定使用哪种曲线。
一个经验法则是,当正类非常少见或者你更关注假正类而不是假负类时,应该选择PR曲线,反之选择ROC曲线。
例如,看前面的ROC曲线图时,以及ROC AUC分数时,你可能会觉得分类器真不错。但这主要是应为跟负类(非5)相比,正类(数字5)的数量真的很少。相比之下,PR曲线清楚地说明分类器还有改进的空间(曲线还可以更接近右上角)
训练一个随机森林分类器,并计算ROC和ROC AUC分数
# 具体RF的原理,第七章介绍from sklearn.ensemble import RandomForestClassifierforest_clf = RandomForestClassifier(n_estimators=10, 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] # score = proba of positive classfpr_forest, tpr_forest, thresholds_forest = roc_curve(y_train_5,y_scores_forest)
plt.figure(figsize=(8, 6))plt.plot(fpr, tpr, "b:", linewidth=2, label="SGD")plot_roc_curve(fpr_forest, tpr_forest, "Random Forest")plt.title("SGD和RL的ROC曲线对比")plt.legend(loc="lower right", fontsize=16)plt.show()
roc_auc_score(y_train_5, y_scores_forest)
0.9931243366003829
测量精度和召回率
y_train_pred_forest = cross_val_predict(forest_clf, X_train, y_train_5, cv=3)precision_score(y_train_5, y_train_pred_forest)
0.9852973447443494
recall_score(y_train_5, y_train_pred_forest)
0.8282604685482383
多类别分类器
二元分类器在两个类别中区分,而多类别分类器(也称为多项分类器),可以区分两个以上的类别。
随机森林算法和朴素贝叶斯分类器可以直接处理多个类别。也有一些严格的二元分类器,比如支持向量分类器或线性分类器。但有多种策略,可以让我们用几个二元二类器实现多类别分类的目的
例如:我们可以训练0-9的10个二元分类器组合,那个分类器给的高,就分为哪一类,这称为一对多(OvA)策略
另一种方法,是为每一对数字训练一个二元分类器:一个用来区分0-1,一个区分0-2,一个区分1-2,依次类推。这称为一对一(OvO)策略,解决N分类,需要(N)*(N-1)/2分类器,比如MNIST问题,需要45个分类器。OvO的主要优点在于每个分类器只需要用到部分训练集对其必须区分的两个类别进行训练。
有些算法(例如支持向量机算法),在数据规模增大时,表现糟糕,因此对于这类算法,OvO是一个优秀的选择,由于在较小的训练集上分别训练多个分类器比在大型数据集上训练少数分类器要快得多。但对于大多数二元分类器,OvA策略还是更好的选择。
# 使用0-9进行训练,在sgd内部,sklearn使用了10个二元分类器,#获得它们对图片的决策分数,然后选择最高的类别sgd_clf.fit(X_train, y_train)sgd_clf.predict([some_digit])
array([5], dtype=int8)
我们可以看到 sgd对输入的结果输出了10个预测分数,而不是1个
some_digit_scores = sgd_clf.decision_function([some_digit])some_digit_scores
array([[-152619.46799791, -441052.22074349, -249930.3138537 ,-237258.35168498, -447251.81933158, 120565.05820991,-834139.15404835, -188142.48490477, -555223.79499145,-536978.92518594]])
np.argmax(some_digit_scores)
5
训练分类器的时候,目标类别的列表会存储在classes_这个属性中,按值的大小进行排序
sgd_clf.classes_
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=int8)
强制使用OVO策略
from sklearn.multiclass import OneVsOneClassifierovo_clf = OneVsOneClassifier(SGDClassifier(max_iter=5, tol=-np.infty, random_state=42))ovo_clf.fit(X_train, y_train)ovo_clf.predict([some_digit])
array([5], dtype=int8)
len(ovo_clf.estimators_)
45
随机森林的多分类,不需要OvA或者OVO策略
forest_clf.fit(X_train, y_train)forest_clf.predict([some_digit])
array([5], dtype=int8)
forest_clf.predict_proba([some_digit])
array([[0.1, 0. , 0. , 0.1, 0. , 0.8, 0. , 0. , 0. , 0. ]])
对分类器进行评估
cross_val_score(sgd_clf, X_train, y_train, cv=3, scoring="accuracy")
array([0.84993001, 0.81769088, 0.84707706])
评测结果大概都为80%以上,如果是随机分类器,准确率大概是10%左右,所以这个结果不是太糟糕,但是依然有提升的空间,比如使用标准化,进行简单的缩放
from sklearn.preprocessing import StandardScalerscaler = StandardScaler()X_train_scaled = scaler.fit_transform(X_train.astype(np.float64))cross_val_score(sgd_clf, X_train_scaled, y_train, cv=3, scoring="accuracy")
array([0.91211758, 0.9099955 , 0.90643597])
错误分析
在这里,假设我们已经找到一个有潜力的模型,现在希望找到一些方法,对其进一步改进。方法之一就是分析其类型错误。
首先,看一下混淆矩阵
y_train_pred = cross_val_predict(sgd_clf, X_train_scaled, y_train, cv=3)conf_mx = confusion_matrix(y_train, y_train_pred)conf_mx
array([[5749, 4, 22, 11, 11, 40, 36, 11, 36, 3],[ 2, 6490, 43, 24, 6, 41, 8, 12, 107, 9],[ 53, 42, 5330, 99, 87, 24, 89, 58, 159, 17],[ 46, 41, 126, 5361, 1, 241, 34, 59, 129, 93],[ 20, 30, 35, 10, 5369, 8, 48, 38, 76, 208],[ 73, 45, 30, 194, 64, 4614, 106, 30, 170, 95],[ 41, 30, 46, 2, 44, 91, 5611, 9, 43, 1],[ 26, 18, 73, 30, 52, 11, 4, 5823, 14, 214],[ 63, 159, 69, 168, 15, 172, 54, 26, 4997, 128],[ 39, 39, 27, 90, 177, 40, 2, 230, 78, 5227]],dtype=int64)
def plot_confusion_matrix(matrix):"""If you prefer color and a colorbar"""fig = plt.figure(figsize=(8,8))ax = fig.add_subplot(111)cax = ax.matshow(matrix)fig.colorbar(cax)
plt.matshow(conf_mx, cmap=plt.cm.gray)plt.show()
5稍微暗一点,可能意味着数据集中5的图片少,也可能是分类器在5上的执行效果不行。实际上,这二者都属实。
让我们把焦点都放在错误上。首先,我们需要将混淆矩阵中的每个值都除以相应类别中的图片数,这样比较的而是错误率,而不是错误的绝对值(后者对图片数量较多的类别不公平)
row_sums = conf_mx.sum(axis=1, keepdims=True)norm_conf_mx = conf_mx / row_sums
行表示实际类别,列表示预测的类别,可以看到 8 9 列比较亮,容易其他数字容易被分错为8 9, 8 9 行业比较亮,说明 8 9 容易被错误分为其他数字。此外3 容易被错分为 5,5也容易被错分为4
np.fill_diagonal(norm_conf_mx, 0) # 填充主对称轴plt.matshow(norm_conf_mx, cmap=plt.cm.gray)plt.show()
分析混淆矩阵,通常可以帮助我们深入了解如何改进分类器。通过上面的图,我们可以花费更多时间来改进8 9的分类,以及修正 3 5 的混淆上。
例如,可以试着收集更多这些数字的训练集,
或者开发新特征来改进分类器–举个例子,写一个算法来计算闭环的数量,比如(8有两个,6有一个,5没有)。
再或者,对图片进行预处理,让某些模式更加突出,比如闭环之类的。
分析单个错误也可以为分类器提供洞察:它在做什么?为什么失败?但这通常更加困难和耗时。例如,我们来看看数字3和数字5的例子:
cl_a, cl_b = 3, 5X_aa = X_train[(y_train == cl_a) & (y_train_pred == cl_a)]X_ab = X_train[(y_train == cl_a) & (y_train_pred == cl_b)]X_ba = X_train[(y_train == cl_b) & (y_train_pred == cl_a)]X_bb = X_train[(y_train == cl_b) & (y_train_pred == cl_b)]plt.figure(figsize=(8,8))plt.subplot(221); plot_digits(X_aa[:25], images_per_row=5)plt.subplot(222); plot_digits(X_ab[:25], images_per_row=5)plt.subplot(223); plot_digits(X_ba[:25], images_per_row=5)plt.subplot(224); plot_digits(X_bb[:25], images_per_row=5)plt.show()
我们可以看到,虽然有一些数字容易混淆,但大多数,还是比较好分类的,但算法还是会分错。因为SGD模型是一个线性模型,它所做的就是为每一个像素分配一个各个类别的权重,当它看到新的图像时,将加权后的像素强度汇总,从而得到一个分数进行分类。而数字3和5只在一部分像素位上有区别,所以分类器很容易将其搞混.
数字3和5之间的主要区别在于连接顶线和下方弧线中间的小线条的位置。如果我们写的数字3将连续点略往左移,分类器就可能将其分类为5,反之亦然。换言之,这个分类器对图像位移和旋转非常敏感,因此,减少3 5混淆的方法之一是对数字进行预处理,确保他们位于中心位置,并且没有旋转。这也有助于减少其他错误。
多标签分类
到目前位置,每个实例都只有一个输出,但某些情况下,我们需要分类器为每个实例产出多个类别,比如,为照片中的每个人脸附上一个标签。
假设分类器经过训练,已经可以识别三张脸 A B C,那么当看到A和C的合照时,应该输出[1,0,1],这种输出多个二元标签的分类系统成为多标签分类系统
下面以k近邻算法为例(不是所有的分类器都支持多标签)
from sklearn.neighbors import KNeighborsClassifiery_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)
KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski',metric_params=None, n_jobs=None, n_neighbors=5, p=2,weights='uniform')
knn_clf.predict([some_digit])
array([[False, True]])
结果正确,5显然小于7,同时是奇数
评估多标签分类器的方法很多,如何选择正确的度量指标取决于我们的项目。比如方法之一是测量每个标签的F1分数(或者是之前讨论过的任何其他二元分类器指标),然后简单的平均。
# 耗时巨大y_train_knn_pred = cross_val_predict(knn_clf, X_train, y_multilabel, cv=3, n_jobs=-1)f1_score(y_multilabel, y_train_knn_pred, average="macro")
0.97709078477525
这里假设了所有的标签都是同等重要,但实际的数据可能并不均衡,可以修改average="weighted"
,来给每个标签设置一个等于其自身支持的权重
多输出分类
现在,我们将讨论最后一种分类任务–多输出多分类任务(简称为多输出分类)。简单而言,它是多标签分类的泛化,其标签也可以是多种类别的(比如有两个以上的值)
说明:构建一个去除图片中噪声的系统。给它输入一个带噪声的图片,它将(希望)输出一张干净的数字图片,跟其他MNIST图片一样,以像素强度的一个数组作为呈现方式。
需要注意的是:这个分类器的输出时多个标签(一个像素点一个标签),每一个标签有多个值(0-255)。所以这是一个多输出分类器系统的例子。
分类和回归之间的界限有时候很模糊,比如这个系统,可以说,预测像素强度更像是回归任务,而不是分类。而多输出系统也不仅仅限于分类任务,可以让一个系统给每个实例输出多个标签,同时包括类别标签和值标签
首先还是从创建训练集和测试集开始,使用Numpy的randint 来给Mnist图片的像素强度增加噪声。目标是将图片还原为原始图片。
noise = np.random.randint(0, 100, (len(X_train), 784))X_train_mod = X_train + noisenoise = np.random.randint(0, 100, (len(X_test), 784))X_test_mod = X_test + noisey_train_mod = X_trainy_test_mod = X_test
some_index = 5500plt.subplot(121); plot_digit(X_test_mod[some_index])plt.subplot(122); plot_digit(y_test_mod[some_index])plt.show()
knn_clf.fit(X_train_mod, y_train_mod)clean_digit = knn_clf.predict([X_test_mod[some_index]])plot_digit(clean_digit)
拓展一:
DummyClassifier(随机预测分类器)
from sklearn.dummy import DummyClassifierdmy_clf = DummyClassifier()y_probas_dmy = cross_val_predict(dmy_clf, X_train, y_train_5, cv=3, method="predict_proba")y_scores_dmy = y_probas_dmy[:, 1]fprr, tprr, thresholdsr = roc_curve(y_train_5, y_scores_dmy)plot_roc_curve(fprr, tprr)
拓展二:
K近邻分类器
# 耗时较大from sklearn.neighbors import KNeighborsClassifierknn_clf = KNeighborsClassifier(n_jobs=-1, weights='distance', n_neighbors=4)knn_clf.fit(X_train, y_train)y_knn_pred = knn_clf.predict(X_test)from sklearn.metrics import accuracy_scoreaccuracy_score(y_test, y_knn_pred)
0.9714
拓展三:
平移图片
from scipy.ndimage.interpolation import shiftdef shift_digit(digit_array, dx, dy, new=0):return shift(digit_array.reshape(28, 28), [dy, dx], cval=new).reshape(784)plot_digit(shift_digit(some_digit, 5, 1, new=100))
c:\users\sha\anaconda3\envs\tensorflow-gpu\lib\importlib\_bootstrap.py:219: RuntimeWarning: numpy.ufunc size changed, may indicate binary incompatibility. Expected 192 from C header, got 216 from PyObjectreturn f(*args, **kwds)
拓展四:
通过平移图片,增强数据集,再训练
X_train_expanded = [X_train]y_train_expanded = [y_train]for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)):shifted_images = np.apply_along_axis(shift_digit, axis=1, arr=X_train, dx=dx, dy=dy)X_train_expanded.append(shifted_images)y_train_expanded.append(y_train)X_train_expanded = np.concatenate(X_train_expanded)y_train_expanded = np.concatenate(y_train_expanded)X_train_expanded.shape, y_train_expanded.shape
((300000, 784), (300000,))
# 耗时巨大,大概几十分钟knn_clf.fit(X_train_expanded, y_train_expanded)y_knn_expanded_pred = knn_clf.predict(X_test)
accuracy_score(y_test, y_knn_expanded_pred)
0.9763
ambiguous_digit = X_test[2589]knn_clf.predict_proba([ambiguous_digit])
array([[0. , 0. , 0.5053645, 0. , 0. , 0. ,0. , 0.4946355, 0. , 0. ]])
plot_digit(ambiguous_digit)
自此我们完成了简单的MINST数据集的一些处理,再通过相近的方法能够完成对MINST数据集中0-9的手写体的分类识别。