文章目录
-
第3章 分类
1 MINIST
2 训练一个二元分类器
3 性能考核
3.1 使用交叉验证测量精度
3.2 混淆矩阵
3.3 精度和召回率
3.4 精度 / 召回率权衡
3.5 ROC曲线及【召回率的本质】
4 多类别分类器
5 错误分析
6 多标签分类
7 多输出分类
8 练习
1. MINIST
这是一组由美国高中生和人口调查局员工手写的70000个数字的图片。每张图像都用其代表的数字标记。被称为机器学习领域的“Hello world ”. sklearn提供了方便的获取 MNIST数据集(github) 的方式.
【 祖传秘制】
# To support both python 2 and python 3
from __future__ import division, print_function, unicode_literals
# Common imports
import numpy as np
import pandas as pd
import os
# to make this notebook's output stable across runs
np.random.seed(42)
# To plot pretty figures
%matplotlib inline
import matplotlib as mpl
import matplotlib.pyplot as plt
mpl.rc('axes', labelsize=14)
mpl.rc('xtick', labelsize=12)
mpl.rc('ytick', labelsize=12)
#多个输出就展示多行
%config ZMQInteractiveShell.ast_node_interactivity="all"
# 忽略无用警告(see SciPy issue #5998)
import warnings
warnings.filterwarnings(action="ignore", message="^internal gelsd")
# Where to save the figures ------ 可修改 start--------
PROJECT_ROOT_DIR = "."
CHAPTER_ID = "classification"
def save_fig(fig_id, tight_layout=True):
path = os.path.join(PROJECT_ROOT_DIR, "images", CHAPTER_ID, fig_id + ".png")
print("Saving figure", fig_id)
if tight_layout:
plt.tight_layout()
plt.savefig(path, format='png', dpi=300)
# Where to save the figures ------ 可修改 end --------
MNIST官方数据集不能在线获取,直接手动下载到本地然后读取:
from scipy.io import loadmat
dataPath = "D:\\0文档\\00机器学习\\0 ML6 _20181022\\Data\\mnist-original.mat"
mnist = loadmat(dataPath)
mnist
查看数据,需要行列转置
X,y = mnist["data"],mnist["label"]
X.shape
y.shape
X = X.T
y = y.T
X.shape
y.shape
共有70000张图片样本,784个特征,即28*28 = 784 个像素点的强度特征。将其重新形成一个28×28数组,用matplotlib的imshow就可以显示出来。
some_digit = X[36000]
some_digit_image = some_digit.reshape(28,28)
plt.imshow(some_digit_image,cmap=mpl.cm.binary,interpolation="nearest")
貌似是5,y[36000],确实是5.
MNIST已经分好了训练集和测试集,所以直接获取
#创建训练集和测试集(MNIST已经建好前60000条样本为训练集)
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]
2. 训练一个二元分类器
做一个判断是不是数字5的二元分类器。label改为True和False:
y_train_5 = (y_train == 5)
y_test_5 = (y_test == 5)
选择随机梯度下降(SGD)分类器
from sklearn.linear_model import SGDClassifier #随机梯度下降分类器
sgd = SGDClassifier(random_state=42)
sgd.fit(X_train,y_train_5)
sgd.predict([some_digit]) #预测之前的数字5
3. 性能考核
评估分类器比评估回归器要困难得多!
3.1 使用交叉验证测量精度
如果希望自己可控性强一点,可以自行实施交叉验证。
from sklearn.model_selection import StratifiedKFold
from sklearn.base import clone
skfolds = StratifiedKFold(n_splits=3, random_state=42)
for train_index,test_index in skfolds.split(X_train,y_train_5):
clone_sgd = clone(sgd) #copy 已训练好的SGD二元分类器
X_train_folds = X_train[train_index] #当前fold的训练集
y_train_folds = (y_train_5[train_index])#当前fold的训练集 label
X_test_fold = X_train[test_index] #当前训练集切分出来的验证集
y_test_fold = (y_train_5[test_index]) #当前训练集切分出来的验证集 label
clone_sgd.fit(X_train_folds,y_train_folds)#重新用当前fold的训练集训练分类器
y_pred = clone_sgd.predict(X_test_fold) #预测验证集
#注意:y_test_fold是一个列表套列表的结构,直接按原文做n_correct是一个列表,所以列表推导式处理了下。
n_correct = sum(y_pred == [x[0] for x in y_test_fold]) #预测正确的样本数量
print("预测准确率:",n_correct/(len(y_pred))) #
输出结果跟sk自带的cross_val_score一致。
from sklearn.model_selection import cross_val_score
cross_val_score(sgd, X_train, y_train_5, cv=3, scoring="accuracy")
但是准确率不能成为分类器的首要性能指标。举个例子:有100个苹果,95个是红苹果,5个是绿苹果,分类器预测一个苹果为红苹果,准确率将达到95%,但是,绿色苹果的准确率确为5%,所以验证拉低了模型的泛化能力,肯定是不行的。 原因就是正负样本的分布不均衡
。
3.2 混淆矩阵
评估分类器性能更好的方法是混淆矩阵。理解它的关键在于弄清楚行列代表的含义:
from sklearn.model_selection import cross_val_predict
from sklearn.metrics import confusion_matrix
y_train_pred = cross_val_predict(sgd,X_train,y_train_5,cv=3)
confusion_matrix(y_train_5,y_train_pred)
最理想的状态就是对角线上的数值都为0。
3.3 精度和召回率
在学习之前一定先搞清楚概念,扫盲
开始:
P : positive 正例,N: negative 负例
注意:
当类别不均衡
时,要将少数的类归为正类。
假设关注的类为正类(正例)
:
TP —— 将正类预测为正类的数量;
FN —— 将正类预测为负类的数量;
FP —— 将负类预测为正类的数量;
TN —— 将负类预测为负类的数量;
精度(Precision)
:P
= TP / TP +FP 或 P = TN/TN+FN,描述的是某一类样本。也可以用负例作为研究指标,
召回率(Recall)
: R
= TP / TP +FN ,描述的是某一类样本。
准确率(accuracy)
: (TP + TN )/( TP + FP + TN + FN),描述的是整体。
F1-score
= 2 / (1/P + 1/R)= 2 * P * R / P + R ,F1-Score是精度和召回率的谐波平均值
,谐波平均值会给予较低的值更高的权重。所以,当精度和召回率都高时,才能得到较高的F1分数(记住结论
)。
from sklearn.metrics import precision_score,recall_score,f1_score
precision_score(y_train_5,y_train_pred)
recall_score(y_train_5,y_train_pred)
f1_score(y_train_5,y_train_pred)
3.4 精度 / 召回率权衡
主要有几种权衡方式
:精度召回率和阈值关系图、PR图(精度和召回率关系)、ROC曲线或AUC。
阈值跟精度成正比,跟召回率成反比。
① 提高阈值:精度提升,召回率降低;
② 降低阈值:精度降低,召回率提升;
sk不允许直接设置阈值,但是可以访问模型用于预测的决策分数
。decision_function() 用于返回每个实例的分数
,然后预测
: 将实例分数分别与阈值进行比较
,大于阈值为正类,小于为负类:
y_scores = sgd.decision_function([some_digit])
y_scores
threshold = 0 # SGD分类器的阈值为 0
y_some_digit_pred = (y_scores > threshold)
y_some_digit_pred
如果提高阈值,则[some_digit]预测为False了,说明提高了阈值,降低了召回率(一提召回率就得联想到真正类率
,正类少了真正类率不就低了,召回率不就降低了)。根据决策分数 和 label,求出精度,召回率,阈值:
y_scores = cross_val_predict(sgd,X_train,y_train_5,cv=3,method="decision_function")
y_scores
#根据决策分数 和 label,求出精度,召回率,阈值
from sklearn.metrics import precision_recall_curve
precisions,recalls,thresholds = precision_recall_curve(y_train_5,y_scores)
precisions,recalls,thresholds
def plot_precision_recall_vs_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_vs_threshold(precisions,recalls,thresholds)
3.5 ROC曲线及召回率的本质
ROC,叫做受试者工作特征曲线(简称ROC),它绘制是的真正类率TPR和假正类率FPR。真负类率TNR(就是粉色部分占N的比例)叫特异度,所以FPR = 1 - 特异度。ROC绘制的是灵敏度和 (1-特异度) 的关系。
上面说了一大堆了,是不是看着有点晕,再来张通俗点的图
:
通过上图和精度召回率的公式,可以看出很多问题哦,比如精度超高,但是召回率超低,你能想到是啥情况么,自己尝试画一下。
如何绘制ROC?
① 计算决策分数y_scores (利用decision_function
或predict_proba
,获取分类器的分数,在sk中就这俩方法):
y_scores = cross_val_predict(sgd,X_train,y_train_5,cv=3,method="decision_function")
② 计算fpr,tpr (利用roc_curve),绘制图:
from sklearn.metrics import roc_curve
fpr,tpr,thresholds = roc_curve(y_train_5,y_scores)
def plot_roc_curve(fpr,tpr):
plt.plot(fpr,tpr,linewidth=2.5)
plt.plot([0,1],[0,1],'k--') #画出对角虚线
plt.axis([0,1,0,1])#设置x轴,y轴的最大最小值
plt.xlabel("FPR")
plt.ylabel("TPR")
plot_roc_curve(fpr,tpr)
③ AUC怎么计算? AUC,area under curve,即曲线下的面积,其实就是ROC曲线下的面积。
# 计算AUC
from sklearn.metrics import roc_auc_score
roc_auc_score(y_train_5,y_scores)
注意:
roc_curve()求出的值是所有示例的tpr,fpr,而roc_auc_score求出的值只有一个,即面积。
问题
:PR曲线和ROC曲线如何选择?PR曲线举例:
选择的经验法则
:当正类非常少(或者更关注假正类)时,应该选择PR曲线。反之,则选择ROC曲线。
最后,我们再训练一个RF分类器,与SGD分类器进行比较。
# 训练RF分类器,与SGD进行ROC的比较
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(random_state=42)
y_proba_rf = cross_val_predict(rf,X_train,y_train_5,cv=3,method="predict_proba")
y_proba_rf #格式为:每个给定实例属于某个给定类别的概率
y_scores_rf = y_proba_rf[:,1] # 获取为正类概率的列表
y_scores_rf
fpr_rf,tpr_rf,thresholds_rf = roc_curve(y_train_5,y_scores_rf)
plt.plot(fpr,tpr,"b:",label="SGD")
plot_roc_curve(fpr_rf,tpr_rf,"Random Forest")
plt.legend(loc="bottom right")
看着ROC,RF要比SGD好很多。我们再看下AUC、精度和召回率,都还不错。
#训练RF
y_train_pred_rf = cross_val_predict(rf,X_train,y_train_5,cv=3)
roc_auc_score(y_train_5,y_scores_rf)
precision_score(y_train_5,y_train_pred_rf)
recall_score(y_train_5,y_train_pred_rf)
4. 多类别分类器
有些算法可以直接处理多个类别,比如随机森林分类器或朴素贝叶斯分类器。但是,有的就不行,比如SVM。
主要有2种实现策略(0~9,10个类别为例):
① OvA one vs all 一对多
策略:训练10个二元分类器,每个数字一个(比如0-检测器,1检测器,等等); 然后获取每个分类器的分数,哪个分数最高就选哪个类别。OvA称为一对多可能有点绕,其实他就是一 对 其他
。
② OvO one vs one 一对一
策略:为每一对数字训练一个二元分类器,比如0和1分类器,0和2分类器,1和3分类器,等等。如果存在N个类别,则就有N * (N-1) ÷ 2个分类器。
对大多数二元分类器来说,OvA是最好的选择,数据规模大了表现糟糕的算法比如SVM有限选OVO。
sgd.fit(X_train,y_train) #没有区分 是否为5 实际sk生成了10个二元分类器
sgd.predict([some_digit])
some_digit_scores = sgd.decision_function([some_digit])
some_digit_scores
maxScore = np.argmax(some_digit_scores) #最高分
sgd.classes_ #所有类别
sgd.classes_[maxScore] #最高分对应的具体类别
有10个分数,说明sk自动生成了10个二元分类器。
如何制定sk使用一对一还是一对多策略呢?则生成一个二元分类器再传入一对一或一对多分类器的构造函数里即可。
from sklearn.multiclass import OneVsOneClassifier #一对一
ovo_clf = OneVsOneClassifier(SGDClassifier(random_state=42))
ovo_clf.fit(X_train,y_train)
ovo_clf.predict([some_digit])
len(ovo_clf.estimators_) #分类评估器数量
随机森林实现多分类
# 随机森林实现多分类
rf_clf = rf.fit(X_train,y_train)
rf_clf.predict([some_digit])
rf_clf.predict_proba([some_digit]) # 将每个实例分类为每个类别的概率
#第5个指数0.8,说明图片是5的概率为80%
cross_val_score(sgd,X_train,y_train,cv=3,scoring="accuracy")
目前准确率超过了80%,做简单的标准化之后,准确率能达到90%:
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X_train.astype(np.float64))
cross_val_score(sgd,X_scaled,y_train,cv=3,scoring="accuracy")
5. 错误分析
假设已经找到了一个有潜力的模型,想进一步改进这个单模,方法之一就是分析其错误类型。
① 查看混淆矩阵并绘图。
y_train_pred = cross_val_predict(sgd,X_scaled,y_train,cv=3)
conf_mx = confusion_matrix(y_train,y_train_pred)
conf_mx
plt.matshow(conf_mx,cmap=plt.cm.Blues_r)
#怎么看? 每行代表实际类别,而每列代表预测类别
使用错误率而不是绝对值,因为绝对值对数量较多的类别不公平:
sum_rows = conf_mx.sum(axis=1,keepdims=True) #获取每个类别的图片数量
norm_conf_mx = conf_mx / sum_rows # 求错误率
np.fill_diagonal(norm_conf_mx,0) #用0填充对角线
plt.matshow(norm_conf_mx,cmap=plt.cm.Blues_r)
② 上图已经找到错误了,那么如何去分析和解决错误呢?
造成数字混淆的原因很明确,就是两个数字图片很像,特征也类似。解决方式无非就两种:一是增加训练样本;二是增加新特征,比如,对图片进行预处理(比如使用sklearn-image、pillow或OpenCV)、增加闭环等新特征。
6. 多标签分类
单标签分类:每个实例都会被分到一个类别里面。
多标签分类:有时候我们想把每个实例分到多个类别里面。比如,一张图片里有张三、李四、王五,那它应该输出[1,0,1]
表示是张三
,不是李四
,是王五
。这种输出多个二元分类标签的分类系统称为多标签分类
。
from sklearn.neighbors import KNeighborsClassifier #不是 所有的分类器都支持 多标签分类
y_train_large = (y_train >= 7) #label大于7的
y_train_odd = (y_train % 2 == 1)#label为奇数的
y_mul = np.c_[y_train_large,y_train_odd] #合并
y_train_large.shape
y_mul.shape
knn_clf = KNeighborsClassifier()
knn_clf.fit(X_train,y_mul)
knn_clf.predict([some_digit])
结果分类正确:5小于7,5是奇数,所以输出为[False,True] 。注意:
并不是 所有的分类器都支持 多标签分类。
评估多标签分类器也可以额用之前的评估方式,比如F1分数。
from sklearn.model_selection import cross_val_predict
from sklearn.metrics import f1_score
#KNN交叉验证跑了半个小时。。。
y_train_knn_pred = cross_val_predict(knn_clf,X_train,y_train,cv=3,n_jobs=-1)
f1_score(y_train,y_train_knn_pred,average="macro")
KNN交叉验证跑了半个小时。。。说明计算量很大,时间复杂度高。KNN算法缺点:
1. 计算量大,尤其是特征数非常多的时候
2. 样本不平衡的时候,对稀有类别的预测准确率低
3. KD树,球树之类的模型建立需要大量的内存
4. 是慵懒散学习方法,基本上不学习,导致预测时速度比起逻辑回归之类的算法慢
5. 相比决策树模型,KNN模型的可解释性不强
7. 多输出分类
最后一种分类任务叫多输出 - 多类别分类(多输出多分类),类似多对多。。咋有这么变态的分类。。
def plot_digit(data):
image = data.reshape(28, 28)
plt.imshow(image, cmap = mpl.cm.binary,
interpolation="nearest")
plt.axis("off")
noise = np.random.randint(0, 100, (len(X_train), 784))
X_train_mod = X_train + noise
noise = np.random.randint(0, 100, (len(X_test), 784))
X_test_mod = X_test + noise
y_train_mod = X_train
y_test_mod = X_test
some_index = 5500
plt.subplot(121); plot_digit(X_test_mod[some_index])
plt.subplot(122); plot_digit(y_test_mod[some_index])
# save_fig("noisy_digit_example_plot")
knn_clf.fit(X_train_mod, y_train_mod)
clean_digit = knn_clf.predict([X_test_mod[some_index]])
plot_digit(clean_digit)