Chap.II Machine Learning 机器学习
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()
为了解决此问题,可透过以下方式:
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()
然而正样本的精确率却是 0 %,意味模型根本无法辨别正样本。
甚么是精确率?
B. Precision and Recall 精确率与召回率
Precision 精确率:被"预测正确样本"中,是"实际正确样本"有多少比例。
Recall 召回率:"实际正确样本"中,被"预测正确样本"有多少比例。
有了精确率与召回率,统计学家进一步定义了:
C. F1 score
化简后可得:
也有学者提出精确率 & 召回率不同权重的算法:
D. ROC 接收者操作特徵曲线
全名 Receiver Operating Characteristic,而曲线下面积称 Area Under Curve (AUC)。
首先定义:
有了这两个指标,再配合模型的"阈值",我们可以画出一条 ROC 曲线。
什么是阈值(Threshold)?
图左:
蓝色为负样本,红色为正样本,横轴则是模型预测的机率。
很直觉的理解:正样本集中于预测机率高的分段上,负样本则较低。
此时我们可以设一个"阈值"(通常预设 0.5),以上判定为正,以下判定为负。
图右:
将横轴设为 FPR,纵轴设为 TPR,配上不同阈值可画出一条曲线。
若选 A 点作阈值,则大部分负样本都被剔除,但正样本也留下较少(TPR/FPR 皆下降)。
约有一半的正样本被判定为正,TPR ≈ 0.5 ,少部分负样本被判定为正,FPR ≈ 0.2 ,
最终得到 A 点在右图曲线上的位置。
AUC 就是计算曲线下的面积,其越大表示模型越好。
若 ROC=0.5,曲线=对角线,模型没有鉴别度。不管正样本判正比例多高,都伴随同比例的负样本被错判。若 ROC=1,则是一完美分类的模型!100% 成功预测出正样本的同时还没有任何的负样本被分类错误。
蛋白质範例的 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()
至此,我们可用更科学的方法来判断模型好坏了!
但问题仍没解决:样本比例差异过大的情况下,总会使训练的模型判断能力差。
因此,接下来我们会尝试透过採样技术来克服它。
5-2. 重组资料
将少数样本用某种方式重複抽样或合成新样本,称过採样。
相反,将多数样本中较不具代表性的移除以免造成杂讯,称欠採样。
A. Oversampling 过採样
A1. SMOTE
全名 Synthetic Minority Oversampling Technique 合成少数过採样技术。
概念是在少数样本位置近的地方,人工合成一些样本。
A. 挑一个少数派(红点),并将邻近的 k 个(k=3)点找出。(Pic1)
B. 从 k 个近邻点中随机选取一个,透过公式合成 N 个(N=3)样本点。(Pic2)
C. 接着对所有的少数点做同样的操作。
蛋白质範例操作 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()
结合刚刚的 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()
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)。
蛋白质範例操作 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. 注意事项
实作上,其实很常同时使用过採样 + 欠採样来做资料重组。如下图:
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
文章参考