Python 演算法 Day 15 - Imbalanced Data

Chap.II Machine Learning 机器学习

https://ithelp.ithome.com.tw/upload/images/20210621/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.target

X_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()

https://ithelp.ithome.com.tw/upload/images/20220103/20138527SrTMdeqf23.png

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

5-1. 评估指标

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

A. Confusion Matrix 混淆矩阵

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

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

https://ithelp.ithome.com.tw/upload/images/20220103/20138527zq79sKRrDM.png

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

甚麽是精确率?

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

Precision 精确率:被"预测正确样本"中,是"实际正确样本"有多少比例。
https://ithelp.ithome.com.tw/upload/images/20211231/20138527nvkB4BfGc3.png

Recall 召回率:"实际正确样本"中,被"预测正确样本"有多少比例。
https://ithelp.ithome.com.tw/upload/images/20211231/20138527akvft0Gw8L.png

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

C. F1 score

https://ithelp.ithome.com.tw/upload/images/20211231/20138527Yz5FjNrpou.png
化简後可得:
https://ithelp.ithome.com.tw/upload/images/20211231/20138527tLxUusM86U.png

也有学者提出精确率 & 召回率不同权重的算法:
https://ithelp.ithome.com.tw/upload/images/20211231/20138527XxGmsgepRJ.png

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

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

首先定义:
https://ithelp.ithome.com.tw/upload/images/20211231/2013852779yujDhVaf.png
有了这两个指标,再配合模型的"阈值",我们可以画出一条 ROC 曲线。

什麽是阈值(Threshold)?

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

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

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

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

蛋白质范例的 ROC(AUC):

from sklearn.metrics import roc_curve, roc_auc_score

fpr, 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()

https://ithelp.ithome.com.tw/upload/images/20220103/20138527e2FYgwOX0p.png

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

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

5-2. 重组资料

将少数样本用某种方式重复抽样或合成新样本,称过采样。
相反,将多数样本中较不具代表性的移除以免造成杂讯,称欠采样。
https://ithelp.ithome.com.tw/upload/images/20220103/20138527yNP8rVAu7f.png

A. Oversampling 过采样

A1. SMOTE

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

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

蛋白质范例操作 SMOTE:

from imblearn.over_sampling import SMOTE
from sklearn.metrics import roc_auc_score, classification_report

X_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()

https://ithelp.ithome.com.tw/upload/images/20220103/20138527EnFJ1h0SEG.png

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

from sklearn.metrics import roc_curve, roc_auc_score

fpr, 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()

https://ithelp.ithome.com.tw/upload/images/20220103/20138527NUNjag9DUU.png

print(auc)

>>  0.7254869736523286

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

A2. Border Line SMOTE

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

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

from imblearn.over_sampling import BorderlineSMOTE
blsmote = 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)。
https://ithelp.ithome.com.tw/upload/images/20220103/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_score

fpr, 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. 注意事项

实作上,其实很常同时使用过采样 + 欠采样来做资料重组。如下图:
https://ithelp.ithome.com.tw/upload/images/20220103/20138527BV0nKsab4j.png

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

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

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

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

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

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

结论:

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

Homework Answer:

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

import numpy as np
import pandas as pd
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split

ds = 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 LogisticRegression
from sklearn.naive_bayes import GaussianNB
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.svm import SVC
from sklearn.neural_network import MLPClassifier
from sklearn.ensemble import RandomForestClassifier

models = []
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_score
from sklearn.model_selection import StratifiedKFold
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA

scores = []
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

文章参考


<<:  D27. 学习基础C、C++语言

>>:  30天程序语言研究

IT铁人DAY 6-UML基本认识

  在进入Pattern的介绍之前,我觉得要先让大家认识一下UML这个东西,尤其是Class Dia...

JavaScript Day 3. 变数:布林、undefined、null

开始学习 JavaScript 之後遇到的变数五花八门,不理解用法或是不懂的回传的型态,就很容易会卡...

Day32 ( 游戏设计 ) 太空狗闪躲陨石

太空狗闪躲陨石 教学原文参考:太空狗闪躲陨石 这篇文章会介绍,如何在 Scratch 3 里使用键盘...

bind9自建DNS解析服务

为了将来可能做DNS负载均衡、或故障转移等,先快速建一个简单的DNS服务,本次安装OS为Centos...

【课程推荐】2021/5/15~5/16 Agile敏捷专案管理实务班

课程目标 本课程将简介两种常见敏捷方法 (Scrum 与 Kanban) 如何在专案中要如何处理问题...