Python 演算法 Day 15 - Imbalanced Data

Chap.II Machine Learning 机器学习

http://img2.58codes.com/2024/20138527JSvpTArTcw.png

https://yourfreetemplates.com/free-machine-learning-diagram/

Part 5. Imbalanced Data 不平衡资料

专案中,常会遇到 Imbalanced Data 不平衡资料。
如:乳癌患者、恐怖份子查验、诈欺犯预测...等,我们关注的是"少数"样本是否能被準确预测?

以蛋白质範例,可以发现决策边界完全无法将少数资料分离。

sns.set(style='white')ds = fetch_datasets()['protein_homo']X = PCA(n_components=2).fit_transform(ds.data)X = MinMaxScaler().fit_transform(X)y = ds.targetX_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42, shuffle=True)# print('X shape:', X.shape)# print('y shape:', y.shape)# print('Positive Ratio:', np.count_nonzero(y==1) / y.shape[0])lr = LogisticRegression().fit(X_train, y_train)y_pred = lr.predict(X_test)# print('Report :', classification_report(y_test, y_pred))# print('ROC :', roc_auc_score(y_test, y_pred))plot_x = np.linspace(0, 1, 1000)plot_y = (-lr.coef_[0][0] * plot_x - lr.intercept_) / lr.coef_[0][1]# 作图sns.scatterplot(X_train[:, 0], X_train[:, 1], hue=y_train)plt.plot(plot_x, plot_y)plt.title('Imblanced Data: Original')plt.ylim(0, 1.5)plt.show()

http://img2.58codes.com/2024/20138527SrTMdeqf23.png

为了解决此问题,可透过以下方式:

5-1. 评估指标

一般来说 Accuracy 準确度是一个直觉性高的指标。
但单纯的準确度并没办法精準衡量模型是好是坏,因此这里介绍几种更常见的评估方式:

A. Confusion Matrix 混淆矩阵

下图为上述资料的混淆矩阵,可发现準确率达到 47700 / (47700+398) = 99.2%

from sklearn.metrics import plot_confusion_matrixcm = plot_confusion_matrix(    lr,    X_test, y_test,    cmap=plt.cm.Blues)plt.show()

http://img2.58codes.com/2024/20138527zq79sKRrDM.png

然而正样本的精确率却是 0 %,意味模型根本无法辨别正样本。

甚么是精确率?

B. Precision and Recall 精确率与召回率

Precision 精确率:被"预测正确样本"中,是"实际正确样本"有多少比例。
http://img2.58codes.com/2024/20138527nvkB4BfGc3.png

Recall 召回率:"实际正确样本"中,被"预测正确样本"有多少比例。
http://img2.58codes.com/2024/20138527akvft0Gw8L.png

有了精确率与召回率,统计学家进一步定义了:

C. F1 score

http://img2.58codes.com/2024/20138527Yz5FjNrpou.png
化简后可得:
http://img2.58codes.com/2024/20138527tLxUusM86U.png

也有学者提出精确率 & 召回率不同权重的算法:
http://img2.58codes.com/2024/20138527XxGmsgepRJ.png

D. ROC 接收者操作特徵曲线

全名 Receiver Operating Characteristic,而曲线下面积称 Area Under Curve (AUC)。

首先定义:
http://img2.58codes.com/2024/2013852779yujDhVaf.png
有了这两个指标,再配合模型的"阈值",我们可以画出一条 ROC 曲线。

什么是阈值(Threshold)?

http://img2.58codes.com/2024/20138527A3zs7y9Y3Y.png
图左:
蓝色为负样本,红色为正样本,横轴则是模型预测的机率。
很直觉的理解:正样本集中于预测机率高的分段上,负样本则较低。
此时我们可以设一个"阈值"(通常预设 0.5),以上判定为正,以下判定为负。

图右:
将横轴设为 FPR,纵轴设为 TPR,配上不同阈值可画出一条曲线。
若选 A 点作阈值,则大部分负样本都被剔除,但正样本也留下较少(TPR/FPR 皆下降)。
约有一半的正样本被判定为正,TPR ≈ 0.5 ,少部分负样本被判定为正,FPR ≈ 0.2 ,
最终得到 A 点在右图曲线上的位置。

AUC 就是计算曲线下的面积,其越大表示模型越好。

若 ROC=0.5,曲线=对角线,模型没有鉴别度。不管正样本判正比例多高,都伴随同比例的负样本被错判。若 ROC=1,则是一完美分类的模型!100% 成功预测出正样本的同时还没有任何的负样本被分类错误。
http://img2.58codes.com/2024/20138527trsFKsiuKg.png

蛋白质範例的 ROC(AUC):

from sklearn.metrics import roc_curve, roc_auc_scorefpr, tpr, threshold = roc_curve(y_test, y_pred)auc = roc_auc_score(y_test, y_pred)plt.title('Receiver Operating Characteristic')plt.plot(fpr, tpr, c='b', label=f'AUC = {auc:0.2f}')plt.plot([0, 1], [0, 1], 'r--')plt.ylabel('True Positive Rate')plt.xlabel('False Positive Rate')plt.legend(loc='best')plt.show()

http://img2.58codes.com/2024/20138527e2FYgwOX0p.png

至此,我们可用更科学的方法来判断模型好坏了!

但问题仍没解决:样本比例差异过大的情况下,总会使训练的模型判断能力差。
因此,接下来我们会尝试透过採样技术来克服它。

5-2. 重组资料

将少数样本用某种方式重複抽样或合成新样本,称过採样。
相反,将多数样本中较不具代表性的移除以免造成杂讯,称欠採样。
http://img2.58codes.com/2024/20138527yNP8rVAu7f.png

A. Oversampling 过採样

A1. SMOTE

全名 Synthetic Minority Oversampling Technique 合成少数过採样技术。
概念是在少数样本位置近的地方,人工合成一些样本。

A. 挑一个少数派(红点),并将邻近的 k 个(k=3)点找出。(Pic1)
B. 从 k 个近邻点中随机选取一个,透过公式合成 N 个(N=3)样本点。(Pic2)
http://img2.58codes.com/2024/20138527hiUe3yGJ4X.png
C. 接着对所有的少数点做同样的操作。
http://img2.58codes.com/2024/20138527hCl4UuGlc5.png

蛋白质範例操作 SMOTE:

from imblearn.over_sampling import SMOTEfrom sklearn.metrics import roc_auc_score, classification_reportX_re, y_re = SMOTE(random_state=42).fit_resample(X_train, y_train)lr = LogisticRegression().fit(X_re, y_re)y_pred = lr.predict(X_test)plot_x = np.linspace(0, 1, 1000)plot_y = (-lr.coef_[0][0] * plot_base - lr.intercept_) / lr.coef_[0][1]# 作图sns.scatterplot(X_re[:, 0], X_re[:, 1], hue=y_re)plt.plot(plot_x, plot_y)plt.title('Positive Sample')plt.ylim(0, 1.5)plt.show()

http://img2.58codes.com/2024/20138527EnFJ1h0SEG.png

结合刚刚的 ROC(AUC)曲线:

from sklearn.metrics import roc_curve, roc_auc_scorefpr, tpr, threshold = roc_curve(y_test, y_pred)auc = roc_auc_score(y_test, y_pred)plt.title('Receiver Operating Characteristic')plt.plot(fpr, tpr, c='b', label=f'AUC = {auc:0.2f}')plt.plot([0, 1], [0, 1], 'r--')plt.ylabel('True Positive Rate')plt.xlabel('False Positive Rate')plt.legend(loc='best')plt.show()

http://img2.58codes.com/2024/20138527NUNjag9DUU.png

print(auc)>>  0.7254869736523286

ROC 显着提升(50% → 72%)!!!

A2. Border Line SMOTE

SMOTE 虽不错,但有一个明显缺点:对"所有少数样本"都做过採样。
大多时候并不是所有少数样本都无鉴别度,真正无鉴别度的是与多数样本混合在一起的少数样本。

靠近边界的少数样本因与多数样本混合在一起,易产生杂讯。若对边界样本学习,可能将多数样本误判为少数。
因此,对 SMOTE 算法做出改进的算法,即 SMOTE Border Line。

from imblearn.over_sampling import BorderlineSMOTEblsmote = BorderlineSMOTE(random_state=42, kind=’borderline-2')X_re, y_re = blsmote.fit_resample(X_train, y_train)

实作上,其实还是更常使用 SMOTE,毕竟 Borderline 方法的计算複杂,且阈值设定也缺乏公定的标準。
需要花更多时间调参,然而跑分进步幅度却不大。

B. Undersampling 欠採样

相对过採样,欠採样是将多数样本进行 Scale Down,使模型的权重改变,少考虑一些多数样本。
最简单的做法是随机排除掉一些多数样本。但有可能排除掉边界样本,
使没鉴别度少数样本也被模型考虑,虽使鉴别度上升,却增加过拟合风险。

B1. Tomek Link

会针对所有样本去遍历一次。
令两个样本点 x, y 分属不同的 class,一个为多数样本,另一为少数,可计算样本间距 d(x, y)。
若找不到第三个样本点 z,使得任一样本点到 z 的距离比样本点间距还小,则删去其。

核心理念:找出边界鉴别度不高的样本,认为这些样本属杂讯应该剔除(类似 Borderline SMOTE)。
http://img2.58codes.com/2024/20138527Ibi28k2kGt.png

蛋白质範例操作 SMOTE + TomekLinks:

X_re, y_re = SMOTE(random_state=42).fit_resample(X_train, y_train)X_rere, y_rere = TomekLinks().fit_resample(X_re, y_re)lr = LogisticRegression().fit(X_rere, y_rere)y_pred = lr.predict(X_test)from sklearn.metrics import roc_curve, roc_auc_scorefpr, tpr, threshold = roc_curve(y_test, y_pred)auc = roc_auc_score(y_test, y_pred)print(auc)>>  0.7332799742949548

可以发现此例中,结合欠採样后并没有让 AUC 显着提升(72% → 73%)。

故专案中通常以过採样为主,欠採样为辅去进行资料重组。

B2. Edited Nearest Neighbor

与 Tomek Links 观念相同,也是透过某种方式来剔除鉴别度低的样本。
ENN 改成对多数样本寻找 K 个近邻点,若一半以上(门槛可自设)不属于多数样本,就将该样本剔除。

5-3. 注意事项

实作上,其实很常同时使用过採样 + 欠採样来做资料重组。如下图:
http://img2.58codes.com/2024/20138527BV0nKsab4j.png

A. 先切分资料,再对训练资料採样。

重新採样的目的是让模型产生鉴别度,而不是让模型学习错误资讯。若先採样才切分,可能使测试资料偏离了原资料,导致模型学习到一堆杂讯。

B. 常透过交叉验证控制过拟合。

不管哪种採样,都会大幅增加过拟合程度(如:样本数少,又做欠採样)。
即使模型区分出来,由于欠採样后多数样本过少,导致模型只侧重学习某部分样本,无法反映资料全貌。
此时,交叉验证、建立多模型做集成学习,都会是好的解决方式。

C. 观察少数样本与多数样本分布情形。

蛋白质範例是因为少数样本与多数样本看上去还能分离,实际运行很有可能碰到完全分不开的例子。
若少数样本杂乱地散落在多数样本之间,此时就不要考虑採样问题。
可以优先评估是否资料本身的分布有问题,像是一开始回收数据错误,或样本并非欧几里得分布等情况。

结论:

面对不平衡资料,需改变判断标準,而不能仅用"準确率"判定模型。若发现 F1 score、ROC 过低,需观察资料分布,才考虑使用过採样/欠採样技术。若有採样,需使用交叉验证控制过拟合。
.
.
.
.
.

Homework Answer:

使用内建 wine,试着用 pipeline、Cross Validation,写个迴圈以操作演示过的演算法。

import numpy as npimport pandas as pdfrom sklearn.datasets import load_winefrom sklearn.model_selection import train_test_splitds = load_wine()X = pd.DataFrame(ds.data, columns=ds.feature_names)y = pd.DataFrame(ds.target, columns=['Wine'])X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)# 把要用的 model 整理出from sklearn.linear_model import LogisticRegressionfrom sklearn.naive_bayes import GaussianNBfrom sklearn.neighbors import KNeighborsClassifierfrom sklearn.tree import DecisionTreeClassifierfrom sklearn.svm import SVCfrom sklearn.neural_network import MLPClassifierfrom sklearn.ensemble import RandomForestClassifiermodels = []models.append(("Logistic Regression", LogisticRegression()))models.append(("Naive Bayes", GaussianNB()))models.append(("K-Nearest Neighbour", KNeighborsClassifier(n_neighbors=3)))models.append(("Decision Tree", DecisionTreeClassifier()))models.append(("Support Vector Machine-linear", SVC(kernel="linear")))models.append(("Support Vector Machine-rbf", SVC(kernel="rbf")))models.append(("Random Forest", RandomForestClassifier(n_estimators=7)))from sklearn.model_selection import cross_val_scorefrom sklearn.model_selection import StratifiedKFoldfrom sklearn.pipeline import make_pipelinefrom sklearn.preprocessing import StandardScalerfrom sklearn.decomposition import PCAscores = []names = []for name, model in models:        kfold = StratifiedKFold(n_splits=10).split(X_train, y_train)        rfc_PL = make_pipeline(    StandardScaler(),    PCA(n_components=2),    model    )        cv = cross_val_score(rfc_PL, X_train, y_train, cv=kfold, scoring = "accuracy")    names.append(name)    scores.append(cv)for i in range(len(names)):    print(f'{names[i]:<30}: {scores[i].mean()*100:.3f}')    >>  Logistic Regression           : 96.090    Naive Bayes                   : 96.090    K-Nearest Neighbour           : 92.949    Decision Tree                 : 94.551    Support Vector Machine-linear : 95.321    Support Vector Machine-rbf    : 96.090    Random Forest                 : 96.090

文章参考


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章