本文本文所述内容,参考了
机器学习实战:基于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
- 使用交叉验证测量精度
可以自己实现:
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%!
- 混淆矩阵
思路是统计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
- 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
召回率(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)
RandomForestClassifier
的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 ]
错误分析
- 查看混淆矩阵
查看混淆矩阵的图像表示:
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()